Skip to content

Commit

Permalink
begin docs, code tweaks
Browse files Browse the repository at this point in the history
  • Loading branch information
jaames committed Aug 26, 2021
1 parent ece6935 commit b05dab7
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 74 deletions.
6 changes: 6 additions & 0 deletions examples/example-basic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
async function usbInit() {
const device = await playdateUsb.requestConnectPlaydate();
await device.open();
await device.screenDebug();
window.device = device;
}
1 change: 1 addition & 0 deletions test/index.html → examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
<body>
<button onclick="usbInit()">connect</button>
<script src="./playdate-usb.js"></script>
<script src="./example-basic.js"></script>
</body>
</html>
24 changes: 23 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "playdate-usb",
"version": "1.0.0",
"description": "",
"description": "JavaScript library for interacting with a Playdate console over USB, wherever webUSB is supported",
"module": "dist/playdate-usb.es.js",
"main": "dist/playdate-usb.js",
"types": "dist/playdate-usb.d.ts",
Expand Down Expand Up @@ -32,5 +32,27 @@
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.30.0",
"typescript": "^4.3.5"
},
"files": [
"dist/**/*"
],
"keywords": [
"playdate",
"panic",
"usb",
"webusb",
"handheld",
"device",
"reversing",
"reverse-engineering"
],
"repository": {
"type": "git",
"url": "git+https://github.com/jaames/playdate-usb.git"
},
"author": "James Daniel <[email protected]>",
"license": "MIT",
"bugs": {
"url": "https://github.com/jaames/playdate-usb/issues"
}
}
92 changes: 87 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,98 @@
# playdate-usb

JavaScript library for interacting with a Playdate console over USB, where [WebUSB](https://web.dev/usb/) is supported.
JavaScript library for interacting with a [Playdate](https://play.date/) console over USB, wherever [webUSB](https://web.dev/usb/) is supported.

## Features

- Send commands to the Playdate and get a response
- Execute secret commands on your Playdate over USB
- Get Playdate device stats such as its version info, serial, cpu stats, memory usage, etc
- Take a screenshot of the Playdate screen and draw it to a HTML5 canvas, or send an image to be previewed on the Playdate screen
- Exports full Typescript types, has zero dependencies, and only ~4kb minified and gzipped

## Notes

WebUSB is only supported in [*Secure Contexts*](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts), and only in [certain browsers]() (Currently Google Chrome, Microsoft Edge, and Opera).

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 been tested on actual Playdate hardware. However, some of the commands that you can potentially run with `runCommand()` 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.

Also, due to the asynchronous nature of webUSB, 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!

## Examples

TODO

## Installation

### With NPM

```shell
npm install playdate-usb --save
```

Then assuming you're using a module-compatible system (like Webpack, Rollup, etc):

```js
import { requestConnectPlaydate } from 'playdate-usb';

async function connectToPlaydate() {
const playdate = await requestConnectPlaydate();
}
```

### Directly in a browser

Using the modules directly via Unpkg:

```html
<script type="module">
import { openDB, deleteDB, wrap, unwrap } from 'https://unpkg.com/playdate-usb?module';
async function connectToPlaydate() {
const playdate = await requestConnectPlaydate();
}
</script>
```

Using an external script reference

```html
<script src="https://unpkg.com/playdate-usb/dist/playdate-usb.min.js"></script>
<script>
async function connectToPlaydate() {
const playdate = await requestConnectPlaydate();
}
</script>
```

When using the library this way, a global called `playdateUsb` will be created containing all the exports from the module version.

## Usage

### Detecting webUSB support

`isUsbSupported()`

### Connecting a Playdate

### PlaydateDevice API

#### `open()`

Opens the device for communication. This will throw an error if the device cannot be opened

## Contributing

If you have any questions or just want to say hi, you can reach me on Twitter ([@rakujira](https://twitter.com/rakujira)), on Discord (`@jaames#9860`), or via email (`mail at jamesdaniel dot dev`).

## Special Thanks

- [Matt Sephton](https://github.com/gingerbeardman) for helping me get access to the Playdate Developer Preview.
- Suz Hinton's [talk about WebUSB at JSConf 2018](https://www.youtube.com/watch?v=IpfZ8Nj3uiE)
- [Matt Sephton](https://github.com/gingerbeardman) for helping me get access to the Playdate Developer Preview
- 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
- Suz Hinton's fun [talk about WebUSB at JSConf 2018](https://www.youtube.com/watch?v=IpfZ8Nj3uiE)
- The folks at [Panic](https://panic.com/) for making such a wonderful and fascinating handheld

----

2021 James Daniel

- https://ssl.engineering.nyu.edu/blog/2018-01-08-WebUSB
Playdate is © [Panic Inc.](https://panic.com/) This project isn't affiliated with or endorsed by them in any way
2 changes: 1 addition & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ module.exports = {
bundleSize(),
// devserver + livereload
devserver && serve({
contentBase: ['dist', 'test']
contentBase: ['dist', 'examples']
}),
devserver && livereload({
watch: 'dist'
Expand Down
125 changes: 78 additions & 47 deletions src/PlaydateDevice.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { Serial } from './Serial';
import { warn, error, assert, splitLines, parseKeyVal, saveAs, bytesToString } from './utils';
import { warn, assert, splitLines, parseKeyVal, bytesToString } from './utils';

// Playdate USB vendor and product IDs
const PLAYDATE_VID = 0x1331;
const PLAYDATE_PID = 0x5740;
export const PLAYDATE_VID = 0x1331;
export const PLAYDATE_PID = 0x5740;

// Playdate screen dimensions
const PLAYDATE_WIDTH = 400;
const PLAYDATE_HEIGHT = 240;
export const PLAYDATE_WIDTH = 400;
export const PLAYDATE_HEIGHT = 240;

/**
* Playdate version information, retrieved by getVersion()
*/
export interface PlaydateVersion {
sdk: string;
build: string;
Expand All @@ -18,6 +22,9 @@ export interface PlaydateVersion {
target: string;
};

/**
* Represents a Playdate device connected over USB, and provides some methods for communicating with it
*/
export class PlaydateDevice {

device: USBDevice;
Expand All @@ -30,55 +37,41 @@ export class PlaydateDevice {
}

/**
* Attempt to pair a Playdate device connected via USB.
* Returns a PlaydateDevice instance upon connection. If no connection could be made, null will be returned instead.
* @returns
* Indicates whether the Playdate is open or close to reading/writing
*/
static async requestDevice() {
try {
assert(window.isSecureContext, `WebUSB is only supported in secure contexts\nhttps://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts`);
assert(navigator.usb !== undefined, `WebUSB is not supported by this browser.\nhttps://developer.mozilla.org/en-US/docs/Web/API/USB#browser_compatibility`);
const device = await navigator.usb.requestDevice({
filters: [{vendorId: PLAYDATE_VID, productId: PLAYDATE_PID }]
});
return new PlaydateDevice(device);
}
catch(e) {
warn(`Could not connect to Playdate: ${ e.message }`);
return null;
}
get isOpen() {
return this.serial.isOpen;
}

/**
* Open a device for communication
*/
async open() {
await this.serial.open();
await this.setEcho('off');
}

/**
* Close a device for communication
*/
async close() {
await this.serial.close();
}

async runCommand(command: string) {
await this.serial.writeAscii(`${ command }\n`);
const str = await this.serial.readAscii();
if (this.logCommandResponse) {
const lines = splitLines(str);
console.log(lines.join('\n'));
}
return str;
}

async help() {
return await this.runCommand('help');
}

/**
* Set the console echo state. By default, this is set to 'off' while opening the device.
*/
async setEcho(echoState: 'on' | 'off') {
const str = await this.runCommand(`echo ${ echoState }`);
assert(str.startsWith('\r\n'), `Invalid echo command response`);
if (echoState === 'off')
assert(str.startsWith('\r\n'), `Invalid echo command response`);
}

/**
* Get version information about the Playdate; its OS build info, SDK version, serial number, etc
*/
async getVersion(): Promise<PlaydateVersion> {
const str = await this.runCommand(`version`);
const str = await this.runCommand('version');
const lines = splitLines(str);
const parsed: Record<string, string> = {};
// split key=value lines into object
Expand Down Expand Up @@ -110,31 +103,47 @@ export class PlaydateDevice {
};
}

/**
* Capture a screenshot from the Playdate, and get the raw framebuffer
* This will return the 1-bit framebuffer data as an Uint8Array of bytes. Each byte in the array will represent 8 pixels
* The framebuffer is 400 x 240 pixels
*/
async getScreen() {
await this.serial.writeAscii(`screen\n`);
await this.serial.writeAscii('screen\n');
const bytes = await this.serial.read();
assert(bytes.byteLength >= 12011, 'Screen command response is too short');
const header = bytesToString(bytes.subarray(0, 11));
const bitmap = bytes.subarray(11, 12011);
const frameBuffer = bytes.subarray(11, 12011);
assert(header.includes('~screen:'), 'Invalid screen command response');
return bitmap;
return frameBuffer;
}

/**
* Capture a screenshot from the Playdate, and get the unpacked framebuffer
* This will return an 8-bit indexed framebuffer as an Uint8Array. Each element of the array will represent a single pixel; `0` for white, `1` for black
* The framebuffer is 400 x 240 pixels
*/
async getScreenIndexed() {
const bitmap = await this.getScreen();
const bitmapSize = bitmap.byteLength;
const framebuffer = await this.getScreen();
const framebufferSize = framebuffer.byteLength;
const indexed = new Uint8Array(PLAYDATE_WIDTH * PLAYDATE_HEIGHT);
let srcPtr = 0;
let dstPtr = 0;
while (srcPtr < bitmapSize) {
const chunk = bitmap[srcPtr++];
for (let b = 7; b >= 0; b--) {
indexed[dstPtr++] = (chunk >> b) & 0x1;
while (srcPtr < framebufferSize) {
const chunk = framebuffer[srcPtr++];
// unpack each bit of the chunk
for (let shift = 7; shift >= 0; shift--) {
indexed[dstPtr++] = (chunk >> shift) & 0x1;
}
}
return indexed;
}

/**
* Capture a screenshot from the Playdate, and get the unpacked RGBA framebuffer
* This will return an 32-bit indexed framebuffer as an Uint32Array. Each element of the array will represent the RGBA color of a single pixel
* The framebuffer is 400 x 240 pixels
*/
async getScreenRgba(palette = [0x000000FF, 0xFFFFFFFF]) {
const indexed = await this.getScreenIndexed();
const rgba = new Uint32Array(indexed.length);
Expand All @@ -144,7 +153,7 @@ export class PlaydateDevice {
return rgba;
}

// TODO: simplify palette to either black+white or approximate
// TODO: simplify palette to either black+white or approximated grey colors
async drawScreenToCanvas(ctx: CanvasRenderingContext2D, palette = [0xFF000000, 0xFFFFFFFF]) {
const indexed = await this.getScreenIndexed();
const imgData = ctx.createImageData(PLAYDATE_WIDTH, PLAYDATE_HEIGHT);
Expand All @@ -164,4 +173,26 @@ export class PlaydateDevice {
await this.drawScreenToCanvas(ctx);
document.body.appendChild(canvas);
}

/**
* Print a list of commands that can be run with `runCommand()`
*/
async help() {
return await this.runCommand('help');
}

/**
* Send a custom USB command to the device
* Some commands are potentially dangerous and could harm your Playdate. *Please* don't execute any commands that you're unsure about.
*/
async runCommand(command: string) {
await this.serial.writeAscii(`${ command }\n`);
const str = await this.serial.readAscii();
if (this.logCommandResponse) {
const lines = splitLines(str);
console.log(lines.join('\n'));
}
return str;
}

}
Loading

0 comments on commit b05dab7

Please sign in to comment.