{"id":13633063,"url":"https://github.com/cranksters/pd-usb","last_synced_at":"2025-04-18T10:33:57.619Z","repository":{"id":42672768,"uuid":"400126782","full_name":"cranksters/pd-usb","owner":"cranksters","description":"JavaScript library for interacting with a Panic Playdate console over USB, wherever WebSerial is supported.","archived":false,"fork":false,"pushed_at":"2024-06-09T21:46:08.000Z","size":189,"stargazers_count":45,"open_issues_count":1,"forks_count":3,"subscribers_count":5,"default_branch":"main","last_synced_at":"2024-10-11T12:18:49.630Z","etag":null,"topics":["handheld","playdate","reverse-engineering","serial","usb","webserial"],"latest_commit_sha":null,"homepage":"https://cranksters.github.io/pd-usb/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cranksters.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-08-26T10:19:01.000Z","updated_at":"2024-10-10T19:30:27.000Z","dependencies_parsed_at":"2024-04-16T11:52:53.000Z","dependency_job_id":"1d16e3e3-3680-4953-a56f-37417e92d44e","html_url":"https://github.com/cranksters/pd-usb","commit_stats":null,"previous_names":["jaames/pd-usb"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cranksters%2Fpd-usb","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cranksters%2Fpd-usb/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cranksters%2Fpd-usb/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cranksters%2Fpd-usb/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cranksters","download_url":"https://codeload.github.com/cranksters/pd-usb/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223779443,"owners_count":17201182,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["handheld","playdate","reverse-engineering","serial","usb","webserial"],"created_at":"2024-08-01T23:00:26.245Z","updated_at":"2024-11-09T02:31:06.347Z","avatar_url":"https://github.com/cranksters.png","language":"TypeScript","funding_links":[],"categories":["Game Development"],"sub_categories":["Programming Frameworks \u0026 Languages"],"readme":"# pd-usb\n\nJavaScript library for interacting with a [Panic Playdate](http://play.date/) console over USB, wherever [WebSerial](https://web.dev/serial/) is supported.\n\n\u003e ⚠️ This library is unofficial and is not affiliated with Panic. Details on the USB protocol were gleaned from reverse-engineering and packet sniffing. Things may be incorrect!\n\n## Features\n\n - Get Playdate device stats such as its version info, serial, cpu stats, etc\n - Grab a screenshot from the Playdate and draw it to a HTML5 canvas, or send an image to be previewed on the device\n - Read the button and crank input state\n - Execute secret commands!\n - Send compiled Lua payloads over USB!\n - Extensive error handling with helpful error messages\n - Exports full Typescript types, has zero dependencies, and weighs less than 5kb minified and gzipped\n\n## Examples\n\n - [Basic Connection](https://cranksters.github.io/pd-usb/example-basic.html)\n - [Input State Capture](https://cranksters.github.io/pd-usb/example-controller.html)\n - [Screenshot Capture](https://cranksters.github.io/pd-usb/example-screen.html)\n - [Send Bitmap](https://cranksters.github.io/pd-usb/example-send-bitmap.html)\n - [Execute Lua Payload](https://cranksters.github.io/pd-usb/example-eval.html)\n\n## Installation\n\n### With NPM\n\n```shell\nnpm install pd-usb --save\n```\n\nThen assuming you're using a module-compatible system (like Webpack, Rollup, etc):\n\n```js\nimport { requestConnectPlaydate } from 'pd-usb';\n\nasync function connectToPlaydate() {\n  const playdate = await requestConnectPlaydate();\n}\n```\n\n### Directly in a browser\n\nUsing the module directly via Unpkg:\n\n```html\n\u003cscript type=\"module\"\u003e\n  import { requestConnectPlaydate } from 'https://unpkg.com/pd-usb?module';\n\n  async function connectToPlaydate() {\n    const playdate = await requestConnectPlaydate();\n  }\n\u003c/script\u003e\n```\n\nUsing an external script reference\n\n```html\n\u003cscript src=\"https://unpkg.com/pd-usb/dist/pd-usb.min.js\"\u003e\u003c/script\u003e\n\u003cscript\u003e\n  async function connectToPlaydate() {\n    const playdate = await pdusb.requestConnectPlaydate();\n  }\n\u003c/script\u003e\n```\n\nWhen using the library this way, a global called `pdusb` will be created containing all the exports from the module version.\n\n## Usage\n\n### Preamble\n\nWebSerial is asynchronous by nature, so this library uses [`async/await`](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await) a lot. If you're not already familiar with that, now would be a good time to catch up!\n\n### Detecting WebSerial support\n\nWebSerial is also only supported in [Secure Contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts), and only in [certain browsers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility) (currently Google Chrome, Microsoft Edge, and Opera). \n\nYou can use the `isUsbSupported()` method to check if the current environment supports WebSerial:\n\n```js\nimport { isUsbSupported } from 'pd-usb';\n\nif (!isUsbSupported) {\n  alert('Sorry, your browser does not support USB, and cannot connect to a Playdate :(')\n}\n```\n\n### Connecting to a Playdate\n\nNext we want to actually connect to a Playdate. Calling `requestConnectPlaydate()` will prompt the user to select a Playdate device from a menu, and returns a [Promise](https://web.dev/promises/) that will resolve a `PlaydateDevice` object if the connection was successful, or reject if a connection could not be made.\n\nFor security reasons, `requestConnectPlaydate()` can only be called with a user interaction, such as a click. It's recommended to create a \"Connect to Playdate\" button somewhere on your page:\n\n```html\n\u003cbutton id=\"connectButton\"\u003eConnect to Playdate\u003c/button\u003e\n```\n\nThen call `requestConnectPlaydate()` in the button's `click` event callback function:\n\n```js\nimport { requestConnectPlaydate } from 'pd-usb';\n\nconst button = document.getElementById('connectButton');\n\nbutton.addEventListener('click', async() =\u003e {\n  try {\n    const device = await requestConnectPlaydate();\n    // do something with device here...\n  }\n  catch (e) {\n    alert('Could not connect to Playdate, lock and unlock the device and try again.');\n  }\n});\n```\n\n### Open and close a PlaydateDevice\n\nBefore interacting with the device, you need to make sure that it is open for communication. This can be done by calling `PlaydateDevice`'s asynchronous `open` method, which returns a promise that resolves when the device is ready, or throws an error if a connection could not be opened.\n\nAfter you are done, it's a good idea to end by calling the `close` method to stop the connection. This function is also asynchronous and returns a Promise that resolves when the device has been closed successfully. Note that browsers seen to handle closing the device when you leave or refresh a page, so it's not the end of the world if you forget this.\n\n```js\nawait device.open();\n\n// interact with the device here...\n\nawait device.close();\n```\n\n### PlaydateDevice events\n\nA `PlaydateDevice` instance will fire events when certain things happen, allowing you to write code to handle things such as the device being disconnected.\n\nYou can add event listeners with the `PlaydateDevice`'s `on` method, and remove then with the `off` method.\n\n```js\nfunction handleDisconnect() {\n  alert('Oh no, the Playdate has been disconnected! Please plug it back in!')\n}\n\n// add an event handler\n// the handleDisconnect function will be called whenever the disconnect event fires\ndevice.on('disconnect', handleDisconnect);\n\n// remove an event handler\ndevice.off('disconnect', handleDisconnect);\n```\n\nThe following events are available:\n\n| Event | Details |\n|:------|:--------|\n| `open` | The device has been opened |\n| `close` | The device has been closed |\n| `disconnect` | The device has been physically disconnected |\n| `controls:start` | Control-polling mode has been started |\n| `controls:update` | A new control state has been received while control-polling mode is active |\n| `controls:stop` | Control-polling mode has been stopped |\n\n### PlaydateDevice general API\n\nThese methods are asynchronous and will resolve when a response has been received from the Playdate, so you need to remember to use `async/await`.\n\n#### `getVersion`\n\nReturns an object containing version information about the Playdate, such as its OS build info, SDK version, serial number, etc.\n\n```js\nconst version = await device.getVersion();\n```\n\n#### `getSerial`\n\nReturns the Playdate's serial number as a string, useful for if you need the user to be able to identify the connected device.\n\n```js\nconst serial = await device.getSerial();\n```\n\n#### `getScreen`\n\nCapture a screenshot from the Playdate, and get the raw framebuffer. This will return the 1-bit framebuffer data as [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) of bytes, where each bit in the byte will represent 1 pixel; `0` for black, `1` for white. The framebuffer is 400 x 240 pixels\n\n```js\nconst screenBuffer = await device.getScreen();\n```\n\n#### `getScreenIndexed`\n\nCapture a screenshot from the Playdate, and get the unpacked framebuffer. This will return an 8-bit indexed framebuffer as an [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array). Each element of the array will represent a single pixel; `0x0` for black, `0x1` for white. The framebuffer is 400 x 240 pixels.\n\n```js\nconst screenPixels = await device.getScreenIndexed();\n```\n\nIf you want to draw the screen to a HTML5 canvas, check out the [screen example](https://github.com/cranksters/pd-usb/blob/main/examples/example-screen.html).\n\n#### `sendBitmap`\n\nSend a 1-bit bitmap buffer to display on the Playdate's screen. The input bitmap must be an [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) of bytes, where each bit in the byte will represent 1 pixel; `0` for black, `1` for white. The input bitmap must also contain 400 x 240 pixels.\n\n```js\nconst screenBuffer = new Uint8Array(12000);\n\n// put some pixels into screenBuffer here\n\nawait device.sendBitmap(screenBuffer);\n```\n\n#### `sendBitmapIndexed`\n\nSend a indexed bitmap to display on the Playdate's screen. The input bitmap must be an [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) of bytes, each byte in the array will represent 1 pixel; `0x0` for black, `0x1` for white. The input bitmap must also contain 400 x 240 pixels.\n\n```js\nconst pixels = new Uint8Array(400 * 240);\n\n// put some pixels into the pixels array here\n\nawait device.sendBitmapIndexed(pixels);\n```\n\nIf you want to creating a bitmap using a HTML5 canvas, check out the [bitmap example](https://github.com/cranksters/pd-usb/blob/main/examples/example-bitmap.html).\n\n#### `run`\n\nLaunch a .pdx file at a given path on the Playdate's data disk. The path must begin with a forward slash, and the device may crash if the selected file does not exist.\n\n```js\nawait device.run('/System/Crayons.pdx');\n```\n\n#### `sendCommand`\n\nSends a plaintext command directly to the Playdate, and returns the response as an array of strings for each line. You can use `await sendCommand('help')` to get a list of all available commands.\n\n\u003e ⚠️ The commands that this library wraps with functions (such as `getVersion()` or `getScreen()`) are known to be safe, are used by the Playdate Simulator, and have all been tested on actual Playdate hardware. However, some of the commands that you can potentially run with `sendCommand()` could be dangerous, and might even harm your favorite yellow handheld if you don't know what you're doing. *Please* don't execute any commands that you're unsure about!\n\n#### `evalLuaPayload`\n\nSends a compiled Lua function to the device to be evaluated. The payload must be a Playdate-compatible Lua function compiled with `pdc` from the Playdate SDK. It will return anything printed to the device's console.\n\n\u003e ⚠️ This is pretty hardcore, you're probably not going to find this useable unless you really know what you're doing. \n\n```js\nconst payloadData = new Uint8Array(... put your payload data here);\nawait device.evalLuaPayload(payloadData);\n```\n\n### Reading Playdate controls\n\nThe Playdate can be put into a control polling mode, where it will send the current state of the buttons and crank at regular intervals. While control polling is active, you won't be able to communicate with the device.\n\nControl polling can be started with `startPollingControls()` and stopped with `stopPollingControls()`. While polling is active, the `controls:update` event will be fired whenever a new control state is received, which seems to be every time the Playdate's update loop is run.\n\n```js\n// add a controls:update event handler\ndevice.on('controls:update', function(state) {\n  // handle control updates here...\n  if (state.buttonDown.b) {\n    console.log('B button is pressed!');\n  }\n});\n\n// start polling controls\nawait device.startPollingControls();\n\n// sometime later...\nawait device.stopPollingControls();\n```\n\nPlease note that disconnecting your Playdate while control polling is active can sometimes cause future USB connections to goof up for a while. This library has code that tries to fix this by clearing the input buffer, but it doesn't seem to be perfect. If this happens, try locking and unlocking your device a few times!\n\n### Getting the control state without an event\n\nYou can also query the Playdate's control state at your own pace, without using events. Note that these methods will only work while control polling is active.\n\n#### `getControls()`\n\nReturns the current control state as an object containing button states and crank angle/dock state.\n\n```js\nconst state = device.getControls();\nconsole.log('B button:', state.pressed.b);\nconsole.log('Crank angle:', state.crank);\n```\n\n#### `buttonIsPressed(button)`, `buttonJustPressed(button)`, `buttonJustReleased(button)`\n\nEquivalents to the Playdate SDK's [button query methods](https://sdk.play.date/1.9.3/Inside%20Playdate.html#_querying_buttons_directly).\n\n`button` should be one of the constants:\n\n- `pbusb.kButtonA`\n- `pbusb.kButtonB`\n- `pbusb.kButtonUp`\n- `pbusb.kButtonDown`\n- `pbusb.kButtonLeft`\n- `pbusb.kButtonRight`\n- `pbusb.kButtonMenu`\n- `pbusb.kButtonLock`\n\nOr one of the strings \"a\", \"b\", \"up\", \"down\", \"left\", \"right\", \"menu\", \"lock\".\n\n#### `isCrankDocked()`, `getCrankPosition()`\n\nEquivalents to the Playdate SDK's [crank query methods](https://sdk.play.date/1.9.3/Inside%20Playdate.html#_querying_crank_status_directly).\n\n## Contributing\n\nContributions and ports to other languages are welcome! Here's a list of things I'd like to do, but haven't found the time yet:\n\n- Node support\n- Figure out how Playdate streaming works\n- Stack traces, memory stats, CPU stats, etc\n- Port to another language to build a general Playdate USB CLI tool?\n\n### USB Docs\n\nIf you're looking for reference, I've documented the Playdate's USB protocol and some of the more interesting commands over on my [playdate-reverse-engineering](https://github.com/cranksters/playdate-reverse-engineering/blob/main/usb/usb.md) repo.\n\n### Setup\n\nTo build the project, you'll need to have Node and NPM installed. Clone the repo to your local machine, then run `npm install` in the project's root directory to grab dependencies. After that you can run `npm start` to begin a dev server on your machine's localhost and point it to the examples directory. You can run `npm run build` to build the production-ready files for distribution.\n\n## Special Thanks\n\n - [Matt](https://github.com/gingerbeardman) for helping me get into the Playdate Developer Preview\n - This [blogpost from Secure Systems Lab](https://ssl.engineering.nyu.edu/blog/2018-01-08-WebUSB) on reverse-engineering USB with WireShark and translating from captured packets to WebUSB calls\n - Suz Hinton's fun [talk about WebUSB at JSConf 2018](https://www.youtube.com/watch?v=IpfZ8Nj3uiE)\n - The folks at [Panic](https://panic.com/) for making such a wonderful and fascinating handheld\n\n----\n\n2021 James Daniel\n\nIf you have any questions or just want to say hi, you can reach me on Twitter ([@rakujira](https://twitter.com/rkgkjr)), on Discord (`@rkgkjr`), or via email (`mail at jamesdaniel dot dev`).\n\nPlaydate is © [Panic Inc.](https://panic.com/) This project isn't affiliated with or endorsed by them in any way\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcranksters%2Fpd-usb","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcranksters%2Fpd-usb","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcranksters%2Fpd-usb/lists"}