Repository: jaames/playdate-reverse-engineering
Branch: main
Commit: dff24945249a
Files: 22
Total size: 94.0 KB
Directory structure:
gitextract_ijazx7lw/
├── .github/
│ └── FUNDING.yml
├── .gitignore
├── LICENSE
├── formats/
│ ├── fnt.md
│ ├── luac.md
│ ├── pda.md
│ ├── pdex.md
│ ├── pdi.md
│ ├── pds.md
│ ├── pdt.md
│ ├── pdv.md
│ ├── pdz.md
│ └── pft.md
├── readme.md
├── server/
│ └── api.md
├── tools/
│ ├── pdex2elf.py
│ ├── pdi2png.py
│ ├── pdz.py
│ └── usbeval.py
└── usb/
├── stream.md
├── usb.md
└── usb_unlocked.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: jaames
================================================
FILE: .gitignore
================================================
testing
tools/*lua
.obsidian
================================================
FILE: LICENSE
================================================
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.
================================================
FILE: formats/fnt.md
================================================
A file with the `.fnt` extension contains font data created by [playdate caps](https://play.date/caps/). It is a line oriented UTF-8 plaintext file format, suitable for editing with normal text editors and optionally includes a base64 encoded PNG sprite sheet.
Each line is one of:
1. String key value data separated by an equals character `=`
2. Glyph widths: A UTF-8 character, whitespace and pixel width of the character
3. Kerning data: Two UTF-8 characters, whitespace and pixel offset for this kerning pair.
4. Lua style comments beginning with `--` and empty lines which are skipped
## Glyph Widths
````
8 8
9 8
space 5
� 9
````
In its most basic form a fnt file is one or more lines comprising an index to specify
characters and widths for each glyphs in an accompanying PNG sprite sheet.
Pairs of UTF-8 glyphs and their widths are separated by any amount of whitespace,
one per line, specified in the order they appear in the sprite sheet.
(Left to Right, Top to Buttom)
Playdate supports all code points in the first four Unicode planes, up to U+3FFFF.
Because these glyphs and widths are whitespace separated, a special string
`space` is substited for the ` ` space glyph.
## External PNG Data
The PNG may be external file or included internally in the fnt file.
When external, the accompanying PNG must be named to match the font file.
For example the PNG for `pantspants.fnt` would named pantspants-table-9-12.png
assuming the fonts glyphs are 9 pixels wide and 12 pixels tall.
## Internal PNG Data
Alternatively the PNG data may be included within the fnt file itself, base64 encoded
along with the necessary metadata (height/width) required to process the PNG sprite sheet.
```
datalen=2052
data=iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAAAXNSR0IArs4c6QAAAF5JREFUOE+tktsKACAIQ/X/P7owGHjJUMm3pJ02k2lei4jYy0OjyBcYyjAmQO/MnLtAOBMdQLoXZ/CIrGPquCb+1KEA4dLMsgsUceb0gCdAQPUc719eXBlc+7qH6dsbK4QPCz6OhZ4AAAAASUVORK5CYII=
```
| Key | Value Detail |
|:-----------|:-----------------------------------------|
| `datalen=` | Integer length of `data` data value which follows (as ASCII numbers)
| `data=` | PNG file data, Base64 encoded
| `width=` | Maximum pixel width of each glyph
| `height=` | Maximum pixel height of each glyph
A 1bit B+W PNG image contains a fixed-size sprite sheet of the individual glyphs.
If the glyphs themselves are narrower than the width their pixels are left justified.
## Kerning Pairs (optional)
```
To -2
Te -4
```
Kerning pairs may be specified, one line per pair with: two characters, whitespace and the offset.
## Tracking info (optional)
| Key | Value Detail |
|:-----------|:-----------------------------------------|
| `tracking=`| Number of pixels of horizontal whitespace between glyphs within a string. Defaults to 1
## CAPS Metadata (optional)
```
--metrics={"baseline":10,"xHeight":0,"capHeight":0,"pairs":{"Te":[-6,0]},"left":[],"right":[]}```
```
The metrics line embeds a JSON object in a lua-style comment to store relevant CAPS editor metadata.
This data is ignored by `pdc`.
The [baseline](https://en.wikipedia.org/wiki/Baseline_(typography)),
[xHeight](https://en.wikipedia.org/wiki/X-height) and
[capHeight](https://en.wikipedia.org/wiki/Cap_height)
are displayed while using the Caps Glyph editor, `left` and `right` contain an array of strings where
each string includes a list of characters which have equivalent left or right-most columns and thus
will use identical kerning information. The pairs object includes kerning pairs specified by the
Caps "Auto-Kern" functionality.
================================================
FILE: formats/luac.md
================================================
Lua-based Playdate games use a tweaked version of Lua 5.4.3. You will only find compiled Lua bytecode in [`.pdz`](/formats/pdz.md) files.
### Compile Flags
`#define LUA_32BITS = 1` set in `luaconf.h`.
### Header Differences
TODO
### Opcode Differences
To maintain backwards-compatibility with Lua 5.4-beta bytecode from [pre-release versions of the Playdate SDK](#pre-sdk-version-180), additional Lua 5.4.3 opcodes were appended to the opcode map like so:
```
LUAI_DDEF const lu_byte luaP_opmodes[NUM_OPCODES] = {
/* MM OT IT T A mode opcode */
opmode(0, 0, 0, 0, 1, iABC) /* OP_MOVE */
,opmode(0, 0, 0, 0, 1, iAsBx) /* OP_LOADI */
,opmode(0, 0, 0, 0, 1, iAsBx) /* OP_LOADF */
,opmode(0, 0, 0, 0, 1, iABx) /* OP_LOADK */
,opmode(0, 0, 0, 0, 1, iABx) /* OP_LOADKX */
,opmode(0, 0, 0, 0, 1, iABx) /* OP_UNKNOWN */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_LOADNIL */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_GETUPVAL */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_SETUPVAL */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_GETTABUP */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_GETTABLE */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_GETI */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_GETFIELD */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_SETTABUP */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_SETTABLE */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_SETI */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_SETFIELD */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_NEWTABLE */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_SELF */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_ADDI */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_ADDK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_SUBK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_MULK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_MODK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_POWK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_DIVK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_IDIVK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_BANDK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_BORK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_BXORK */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_SHRI */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_SHLI */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_ADD */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_SUB */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_MUL */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_MOD */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_POW */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_DIV */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_IDIV */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_BAND */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_BOR */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_BXOR */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_SHL */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_SHR */
,opmode(1, 0, 0, 0, 0, iABC) /* OP_MMBIN */
,opmode(1, 0, 0, 0, 0, iABC) /* OP_MMBINI*/
,opmode(1, 0, 0, 0, 0, iABC) /* OP_MMBINK*/
,opmode(0, 0, 0, 0, 1, iABC) /* OP_UNM */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_BNOT */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_NOT */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_LEN */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_CONCAT */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_CLOSE */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_TBC */
,opmode(0, 0, 0, 0, 0, isJ) /* OP_JMP */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_EQ */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_LT */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_LE */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_EQK */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_EQI */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_LTI */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_LEI */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_GTI */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_GEI */
,opmode(0, 0, 0, 1, 0, iABC) /* OP_TEST */
,opmode(0, 0, 0, 1, 1, iABC) /* OP_TESTSET */
,opmode(0, 1, 1, 0, 1, iABC) /* OP_CALL */
,opmode(0, 1, 1, 0, 1, iABC) /* OP_TAILCALL */
,opmode(0, 0, 1, 0, 0, iABC) /* OP_RETURN */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_RETURN0 */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_RETURN1 */
,opmode(0, 0, 0, 0, 1, iABx) /* OP_FORLOOP */
,opmode(0, 0, 0, 0, 1, iABx) /* OP_FORPREP */
,opmode(0, 0, 0, 0, 0, iABx) /* OP_TFORPREP */
,opmode(0, 0, 0, 0, 0, iABC) /* OP_TFORCALL */
,opmode(0, 0, 0, 0, 1, iABx) /* OP_TFORLOOP */
,opmode(0, 0, 1, 0, 0, iABC) /* OP_SETLIST */
,opmode(0, 0, 0, 0, 1, iABx) /* OP_CLOSURE */
,opmode(0, 1, 0, 0, 1, iABC) /* OP_VARARG */
,opmode(0, 0, 1, 0, 1, iABC) /* OP_VARARGPREP */
,opmode(0, 0, 0, 0, 0, iAx) /* OP_EXTRAARG */
// opcodes appended:
,opmode(0, 0, 0, 0, 1, iABC) /* OP_LOADFALSE */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_LFALSESKIP */
,opmode(0, 0, 0, 0, 1, iABC) /* OP_LOADTRUE */
};
```
## Pre SDK Version 1.8.0
Prior to SDK version 1.8.0 (which was only available to developer preview members - 1.9.0 was the first publicly available SDK release), the Playdate Lua runtime was based on [prerelease/beta version of Lua 5.4](https://github.com/lua/lua/tree/6c0e44464b9eef4be42e2c8181aabfb3301617ad). This version used a slightly nonstandard bytecode header structure.
It is possible to execute Playdate Lua bytecode from this version of the SDK by compiling the Lua 5.4-beta with `#define LUA_32BITS = 1` set in `luaconf.h`.
### Header
| Offset | Type | Detail |
|:-------|:--------|:-------|
| `0` | `byte[4]` | Constant `LUA_SIGNATURE` (hex `1B 4C 75 61`) |
| `4` | `uint16` | Version (`0x03F8` = 5.4.0 prerelease) |
| `6` | `byte` | Constant `LUAC_FORMAT` (hex `00`) |
| `7` | `byte[6]` | Constant `LUAC_DATA` (hex `19 93 0D 0A 1A 0A`) |
| `13` | `uint8` | Instruction size (always `4`) |
| `14` | `uint8` | Integer size (always `4`) |
| `15` | `uint8` | Number size (always `4`) |
| `16` | `lua int` | Constant `LUA_INT` (`0x5678`) |
| `20` | `lua float` | Constant `LUA_NUM` (`370.5`) |
================================================
FILE: formats/pda.md
================================================
A file with the `.pda` extension represents audio data that has been compiled by `pdc`. This format uses little endian byte order.
## Header
| Offset | Type | Detail |
|:-------|:---------|:-------|
| `0` | `char[12]` | Ident "Playdate AUD" |
| `12` | `uint24` | Sample rate (in Hz) |
| `15` | `uint8` | [Audio data format](#audio-data-format) |
### Audio Data Format
The audio data format field in the file header seems to map to the `playdate.sound` constants in the official SDK:
| Value | SDK Constant | Detail |
|:------|:-------------|:-------|
| `0` | `kFormat8bitMono` | unsigned 8-bit PCM, one channel |
| `1` | `kFormat8bitStereo` | unsigned 8-bit PCM, two channels |
| `2` | `kFormat16bitMono` | signed 16-bit little endian PCM, one channel |
| `3` | `kFormat16bitStereo` | signed 16-bit little endian PCM, two channels |
| `4` | `kFormatADPCMMono` | 4-bit IMA ADPCM, one channel |
| `5` | `kFormatADPCMStereo` | 4-bit IMA ADPCM, two channels |
## Audio Data
The format flag in the file header indicates how the audio is stored:
### 4-bit IMA ADPCM
In this format, the audio is encoded in blocks. The audio data begins with an `uint16` which gives the size of a single block, and the start of the first block begins immediately after.
Each block also begins with a small header consisting of 4 bytes for each audio channel:
| Type | Detail |
|:-------|:-------|
| `uint16` | ADPCM predictor |
| `uint8` | ADPCM step index |
| `uint8` | Reserved, should always be zero |
The rest of the block contains regular 4-bit IMA ADPCM audio samples. For stereo audio, the left channel uses the high nibble of every byte, while the right channel uses the low nibble.
### 8-bit PCM
Standard 8-bit PCM. Each sample can be converted to signed 16-bit PCM with `(sample - 0x80) << 8`. For stereo audio, the channels are interleaved so you read one sample for the left channel, the next one for the right, next one for the left, and so on.
### 16-bit PCM
Standard signed 16-bit PCM in little-endian byte order. As with the 8-bit format, for stereo audio, the channels are interleaved so you read one sample for the left channel, the next one for the right, next one for the left, and so on.
================================================
FILE: formats/pdex.md
================================================
The `pdex.bin` file represents information and executable code copied by `pdc` from a `pdex.elf` ELF file compiled for ARM32 (Thumb, EABI v5, hard-float). It is usually the entry point of Playdate games created using the C API. The file format uses little endian byte order.
Supplementary reading:
- https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
- https://www.man7.org/linux/man-pages/man5/elf.5.html
# File structure
1. [File header](#file-header)
2. [Program header](#program-header)
3. [Program data](#program-data)
1. [Program segment](#program-segment)
2. [Relocation entries](#relocation-entries)
## File header
| Offset | Type | Detail |
|:-------|:-----------|:-------------------------------|
| `0x00` | `char[12]` | File signature: `Playdate PDX` |
| `0x0C` | `uint32` | [Flags](#flags) |
| `0x10` | - | End of file header (size) |
### Flags
| Bitmask | Detail |
|:--------------------|:------------------------------------------------------|
| `flag & 0x40000000` | If `> 0`, all data after the file header is encrypted |
Encryption is (at the time of writing) only used by Catalog games as a form of DRM. The encryption method is not yet known.
## Program header
| Offset | Type | Detail |
|:-------|:------------|:---------------------------------------------------|
| `0x00` | `uint8[16]` | MD5 checksum of program segment |
| `0x10` | `uint32` | Size of program segment in file image; `p_filesz` |
| `0x14` | `uint32` | Size of program segment in memory image; `p_memsz` |
| `0x18` | `uint32` | Entry point address; `e_entry` |
| `0x1C` | `uint32` | Number of relocation entries |
| `0x20` | - | End of program header (size) |
## Program data
The program data is zlib-compressed and consists of [a single program segment](#program-segment) immediately followed by [relocation entries](#relocation-entries).
### Program segment
The first `p_filesz` bytes of the uncompressed program data is the program segment, usually consisting of the `.text` (executable code) and `.data` (initialized global variables) sections of the original ELF file. The `.bss` (uninitialized global variables) section does not occupy any space in the file image; its size in memory can be computed via `p_memsz - p_filesz`.
### Relocation entries
The next `<number of relocation entries> * 4` bytes of the uncompressed program data are the relocation entries, usually from the `.rel.text` and/or `.rel.data` sections of the original ELF file. Each entry is a single `uint32` denoting a byte offset from the beginning of the program segment where a relocation should take place, corresponding to the `r_offset` member of an `Elf32_Rel` relocation entry.
================================================
FILE: formats/pdi.md
================================================
A file with the `.pdi` extension represents a 1-bit bitmap image that has been compiled by `pdc`. This format uses little endian byte order.
## Header
| Offset | Type | Detail |
|:-------|:----------|:---------------------|
| `0` | `char[12]` | Ident `Playdate IMG` |
| `12` | `uint32` | [Flags](#flags) |
### Flags
| Bitmask | Detail |
|:--------------------|:--------------------------------------------|
| `flags & 0x80000000` | If `> 0`, the data in this file is compressed |
## Image Header
If the compression flag is set, this image header follows the file header. Everything after the image header is zlib-compressed.
| Offset | Type | Detail |
|:-------|:---------|:--------------------------------|
| `0` | `uint32` | Size of image data section when decompressed |
| `4` | `uint32` | Image width (in pixels) |
| `8` | `uint32` | Image height (in pixels) |
| `12` | `uint32` | Unknown/reserved? Seen as 0 |
## Image Data
`.pdi` image data comprises of a single [Image Cell](#image-cell).
## Image Cell
The `pdi`, [`.pdt`](formats/pdi.md) and [`.pft`](formats/pft.md) formats store pixels as "cells", where transparent edges are cropped out to save on space.
### Cell Header
| Offset | Type | Detail |
|:-------|:---------|:-------|
| `0` | `uint16` | Cell clip width (in pixels) |
| `2` | `uint16` | Cell clip height (in pixels) |
| `4` | `uint16` | Cell stride (bytes per image row) |
| `6` | `uint16` | Cell clip left (in pixels) |
| `8` | `uint16` | Cell clip right (in pixels) |
| `10` | `uint16` | Cell clip top (in pixels) |
| `12` | `uint16` | Cell clip bottom (in pixels) |
| `14` | `uint16` | [Cell bitflags](#cell-bitflags) |
### Cell Bitflags
| Bitmask | Detail |
|:--------------------|:---------------------------|
| `flags & 0x3` | If `> 0`, cell uses transparency |
### Cell Pixels
Cells contain at least one 1-bit bitmap for black/white color (`0` for black and `1` for white). If the transparency flag is set, this will be followed by an additional 1-bit bitmap for the image alpha (`0` for transparent and `1` for opaque).
The number of bytes used by a cell bitmap will be equal to `stride * clip height`. The number of pixels in each row of the cell bitmap will be equal to `clip width`. Transparent edges are not stored, and must be added back to the cell based on the values given in the cell header. The stride is always a multiple of 4, ensuring that the data can be accessed with 32-bit reads.

The final image width will equal `clip left + clip width + clip right`, likewise the height will equal `clip top + clip height + clip bottom`,
================================================
FILE: formats/pds.md
================================================
A file with the `.pds` extension represents a collection localization strings that have been compiled by `pdc`. This format uses little endian byte order.
## Header
| Offset | Type | Detail |
|:-------|:---------|:-------|
| `0` | `char[12]` | Ident "Playdate STR" |
| `12` | `uint32` | [Flags](#flags) |
### Flags
| Bitmask | Detail |
|:--------------------|:--------------------------------------------|
| `flag & 0x80000000` | If `> 0`, the data in this file is compressed |
### String header
If the compression flag is set, there's an extra string data header after the file header. Everything after this is zlib-compressed.
| Offset | Type | Detail |
|:-------|:---------|:-------|
| `0` | `uint32` | Size of decompressed string data |
| `4` | `uint32` | Unused/reserved, seen as 0 |
| `8` | `uint32` | Unused/reserved, seen as 0 |
| `12` | `uint32` | Unused/reserved, seen as 0 |
## String Data
### Table Header
| Offset | Type | Detail |
|:-------|:--------|:-------|
| `0` | `uint32` | Number of string entries |
### Table
After this header, there is a table of int32 offsets for each string entry aside from the first one, as well as an offset to the end of the data.
Offsets are relative to the end of the table, and the first string entry always begins directly after the table.
### String Entries
Each string entry contains an utf8 string key, followed by a null byte, followed by a utf8 string value, followed by another null byte.
================================================
FILE: formats/pdt.md
================================================
A file with the `.pdt` extension represents a 1-bit bitmap image table containing multiple sub-images (like a spritesheet or animation) that has been compiled by `pdc`. This format uses little endian byte order.
## Header
| Offset | Type | Detail |
|:-------|:---------|:-------|
| `0` | `char[12]` | Ident `Playdate IMT` |
| `12` | `uint32` | [Flags](#flags) |
### Flags
| Bitmask | Detail |
|:--------------------|:--------------------------------------------|
| `flag & 0x80000000` | If `> 0`, the data in this file is compressed |
## Image Header
If the compression flag is set, there's an extra header after the file header:
| Offset | Type | Detail |
|:-------|:---------|:--------------------------------|
| `0` | `uint32` | Size of decompressed image data |
| `4` | `uint32` | Image width (in pixels) |
| `8` | `uint32` | Image height (in pixels) |
| `12` | `uint32` | Number of cells |
The image width and height are for the first image only. In sequential image tables, the following images may be of
different sizes. In matrix image tables, all images must be the same size.
If the compression flag is set, then this section is zlib-compressed.
## Image Data
### Table Header
| Offset | Type | Detail |
|:-------|:--------|:-------|
| `0` | `uint16` | Num cells |
| `2` | `uint16` | Num cells per row |
For sequential image tables, the values will be the same. For matrix image tables, the second value will be the number of cells on each row.
### Table
After this header, there is a table of uint32 offsets for each [image cell](#image-cell) aside from the first one, as well as an offset to the end of the data.
Offsets are relative to the end of the table, and the first cell always begins directly after the table.
### Image Cell
See: [Image Cell](/formats/pdi.md#image-cell)
================================================
FILE: formats/pdv.md
================================================
A file with the `.pdv` extension represents a 1-bit video that has been converted by `1bitvideo.app`. This format uses little endian byte order.
## Header
| Offset | Type | Detail |
|:-------|:---------|:-------|
| `0` | `char[12]` | Ident `Playdate VID` |
| `12` | `uint32` | Reserved, always 0 |
| `16` | `uint16` | Number of frames |
| `18` | `uint16` | Reserved, always 0 |
| `20` | `float32` | Framerate, measured in frames per second |
| `24` | `uint16` | Frame width (in pixels) |
| `26` | `uint16` | Frame height (in pixels) |
In 1bitvideo.app the frame width and height seem to be hardcoded to `400` and `240` respectively, at least at the time of writing.
## Frame Table
Following the header is a series of `uint32` values, one for each frame, and one additional to mark the end of the data. So if the number of frames is 16, there will be 17 entries in this table. These values contain the frame data offset as well as the frame type:
| Value | Detail |
|:------|:-------|
| `value >> 2` | Offset |
| `value & 0x3` | Frame type |
### Frame Types
| Type | Detail |
|:-----|:-------|
| `0` | No frame |
| `1` | [I-frame](https://en.wikipedia.org/wiki/Video_compression_picture_types) |
| `2` | [P-frame](https://en.wikipedia.org/wiki/Video_compression_picture_types) |
| `3` | Combined I-frame and P-frame |
A `0` type frame is placed at the end to identify where the preceeding frame's data ends. There is no actual data following it.
## Frame Data
Frame data begins immediately after the frame table. Each frame is z-lib compressed separately. Decompressed, the frame contains a 1-bit pixel map where `0` is black and `1` is white.
### P-frames
Frame type `2` is for P-frames (frames that are based on previous frames), and these only store the pixels that have changed since the previous frame. The full image can be resolved by looping through each pixel in the frame and doing a logical XOR against the same pixel from the previous resolved frame.
For example in C this would be something like:
```c
for (int i = 0; i < sizeof(frame); i++)
{
frame[i] ^= prevFrame[i];
}
```
### Combined I-frame and P-frame
Frame type 3 contains both I-frame and P-frame data for the same frame. This is so you can step backwards from an I-frame without having to jump to the previous I-frame then apply P-frames all the way forward.
The frame data for this frame will start with an `uint16` giving the length of the I-frame data, followed by the I-frame data, and then the P-frame data.
================================================
FILE: formats/pdz.md
================================================
A file with the `.pdz` extension represents a file container that has been compiled by `pdc`. They mostly contain compiled Lua bytecode, but they can sometimes include other assets such as images or fonts. This format uses little endian byte order.
## Header
| Offset | Type | Detail |
|:-------|:---------|:-------|
| `0` | `char[12]` | Ident `Playdate PDZ` |
| `12` | `uint32` | [Flags](#flags) |
### Flags
| Bitmask | Detail |
|:--------------------|:--------------------------------------------|
| `flag & 0x40000000` | If `> 0`, the data in this file is encrypted |
File encryption is (at the time of writing) only used by Catalog games' `main.pdz` file as a form of DRM. The encryption method isn't known.
## File Entries
Following the header is a list of file entries. Each entry has a header.
| Type | Detail |
|:--------|:-------|
| `uint8` | [Entry Flags](#entry-flags) |
| `uint24` | [Entry Data](#entry-data) length |
| `string` | Filename as null-terminated C string |
| `-` | Optional null-padding if needed to align to the next multiple of 4 bytes |
If the [Entry Type](#entry-type) flag is `5` (for a `.pda` audio file), some additional values are included:
| Type | Detail |
|:--------|:-------|
| `uint24` | Audio sample rate in Hz |
| `uint8` | [Audio Data Format](/format/pda.md#audio-data-format) |
### Entry Flags
| Flag | Detail |
|:-------|:-------|
| `flags & 0x80` | If `> 0`, file entry data is compressed |
| `flags & 0x7F` | [Entry Type](#entry-type) |
### Entry Type
| Flag | Detail |
|:-------|:-------|
| `0` | Unknown/unused |
| `1` | Compiled Lua bytecode ([`.luac`](/formats/luac.md)) |
| `2` | Static image ([`.pdi`](/formats/pdi.md)) |
| `3` | Animated image ([`.pdt`](/formats/pdt.md)) |
| `4` | Video ([`.pdv`](/formats/pdv.md)) |
| `5` | Audio ([`.pda`](/formats/pda.md)) |
| `6` | Text strings ([`.pds`](/formats/pds.md)) |
| `7` | Font ([`.pft`](/formats/pft.md)) |
## Entry Data
The data for a given file entry is immediately after the entry's file header. If the file's compression flag is set, this will begin with a `uint32` giving the decompressed size of the data, followed by zlib-compressed data.
All of the asset entries (`.pdi`, `.pdt`, `.pdv`, `.pda`, `.pds`, `.pft`), will be missing the first 16 bytes of the header, since for most of these formats this just contains a 12-byte format ident string and some compression flags. This is why `.pda` entries have additional header fields for the sample rate and audio format.
================================================
FILE: formats/pft.md
================================================
A file with the `.pft` extension represents a 1-bit bitmap font that has been compiled by `pdc`. This format uses little endian byte order.
## Header
| Offset | Type | Detail |
|:-------|:----------|:---------------------|
| `0` | `char[12]` | Ident `Playdate FNT` |
| `12` | `uint32` | [Flags](#flags) |
### Flags
| Bitmask | Detail |
|:--------------------|:--------------------------------------------|
| `flags & 0x80000000` | If `> 0`, the data in this file is compressed |
| `flags & 0x00000001` | If `> 0`, the font contains characters above U+1FFFF |
## Font Header
If the compression flag is set, there's an extra font header after the file header. Everything after this is zlib-compressed.
| Offset | Type | Detail |
|:-------|:---------|:--------------------------------|
| `0` | `uint32` | Size of font data section when decompressed |
| `4` | `uint32` | Maximum glyph width (in pixels) |
| `8` | `uint32` | Maximum glyph height (in pixels) |
## Page List
### Page List Header
| Offset | Type | Detail |
|:-------|:---------|:--------------------------------|
| `0` | `uint8` | Glyph width (in pixels) |
| `1` | `uint8` | Glyph height (in pixels) |
| `2` | `uint16` | Tracking (in pixels) |
| `4` | `64 bytes` | [Page Usage Flags](#page-usage-flags) |
Font glyphs are grouped into pages based on their unicode codepoints. Each page covers a span of 256 glyphs. Pages are only stored if they have glyphs present in the font.
The page index for a given glyph codepoint will be `codepoint >> 8`.
Following this header is a list of `uint32` offsets for all of the pages present in the file, with the pages following immediately after.
### Page Usage Flags
This contains bitflags for each page, starting at the lowest significant bit in the first byte. If a page's corresponding usage flag is `1`, then it is present in the file.
## Page
### Page Header
| Offset | Type | Detail |
|:-------|:---------|:--------------------------------|
| `0` | `uint24` | Reserved? Seen as `0` |
| `3` | `uint8` | Number of glyphs |
| `4` | `32 bytes` | [Glyph Usage Flags](#glyph-usage-flags) |
After the page header is a series of [Glyphs](#glyph) for the page.
### Glyph Usage Flags
This contains bitflags for each glyph, starting at the lowest significant bit in the first byte. If a glyph's corresponding usage flag is `1`, then it is present in the page.
## Glyph
Each glyph is comprised of:
- a header
- a short kerning table
- (if necessary) padding bytes to align to the next multiple of 4
- a long kerning table
- pixel data
### Glyph Header
| Offset | Type | Detail |
|:-------|:---------|:--------------------------------|
| `0` | `uint8` | Glyph advance / width (in pixels) |
| `1` | `uint8` | Number of [Short Kerning Table Entries](#short-kerning-table-entries) |
| `2` | `uint16` | Number of [Long Kerning Table Entries](#long-kerning-table-entries) |
### Short Kerning Table Entries
I think this is for codepoints within the same page?
| Offset | Type | Detail |
|:-------|:---------|:--------------------------------|
| `0` | `uint8` | Other glyph codepoint |
| `1` | `int8` | Kerning (in pixels) |
### Long Kerning Table Entries
This supports any unicode codepoint within the whole font
| Offset | Type | Detail |
|:-------|:---------|:--------------------------------|
| `0` | `uint24` | Other glyph codepoint |
| `3` | `int8` | Kerning (in pixels) |
### Glyph pixels
Stored as an [Image Cell](/formats/pdi.md#image-cell).
================================================
FILE: readme.md
================================================
Unofficial Playdate reverse-engineering notes/tools - covers file formats, server API and USB serial commands
> ⚠️ This documentation is unofficial and is not affiliated with Panic. All of the content herein was gleaned from reverse-engineering Playdate tools and game files, and as such there may be mistakes or missing information.
## Documentation
- **File Formats**
- **Playdate game formats**
- [**pdex.bin**](formats/pdex.md) - Executable code
- [**.luac**](formats/luac.md) - Lua bytecode
- [**.pdz**](formats/pdz.md) - File container
- [**.pda**](formats/pda.md) - Audio file
- [**.pdi**](formats/pdi.md) - Image file
- [**.pdt**](formats/pdt.md) - Imagetable file
- [**.pdv**](formats/pdv.md) - Video file
- [**.pds**](formats/pds.md) - Strings file
- [**.pft**](formats/pft.md) - Font file
- **Other formats**
- [**.fnt**](formats/fnt.md) - Font source file
- **.strings** - Strings source file (TODO)
- **Server**
- [**Playdate API**](server/api.md) - Main Playdate server API
- **Misc**
- [**USB**](usb/usb.md) - USB serial interface
- [**Streaming**](usb/stream.md) - Video/audio streaming protocol (via USB serial), used by Playdate Mirror
## Tools
- [**`pdz.py`**](tools/pdz.py) - Unpacks all files from a `.pdz` file container.
- [**`pdex2elf.py`**](tools/pdex2elf.py) - Converts a `pdex.bin` to an ELF file that can be analyzed in tools such as readelf, objdump or Ghidra, or compiled back to the same original `pdex.bin` by `pdc`.
- [**`pdi2png.py`**](tools/pdi2png.py) - Converts a `.pdi` image file to a `.png` image.
- [**`usbeval.py`**](tools/usbeval.py) - Uses the Playdate's USB `eval` command to evaluate a Lua script over USB. Has access to the Lua runtime of the currently loaded game, except for system apps.
## Related Projects and Resources
- [**pd-usb**](https://github.com/jaames/pd-usb) - JavaScript library for interacting with the Playdate's serial API from a WebUSB-compatible web browser.
- [**unluac**](https://github.com/scratchminer/unluac) - Fork of the unluac Lua decompiler, modified to support Playdate-flavoured Lua.
- [**lua54**](https://github.com/scratchminer/lua54) - Fork of Lua that aims to match the custom tweaks that Panic added for Playdate-flavoured Lua.
## Special Thanks
- [Zhuowei](https://github.com/zhuowei) for this [script for unpacking Playdate .pdx executables](https://gist.github.com/zhuowei/666c7e6d21d842dbb8b723e96164d9c3), which was the base for `pdz.py`
- [Scratchminer](https://github.com/scratchminer) for their further reverse-engineering work on the Playdate's [file formats](https://github.com/scratchminer/pd-emu), streaming protocol and [Lua implementation](https://github.com/scratchminer/lua54).
- [Simon](https://github.com/simontime) for helping with some ADPCM audio data reverse engineering
- The folks at [Panic](https://panic.com/) for making such a wonderful and fascinating handheld!
----
2022-2023 James Daniel
Playdate is © [Panic Inc.](https://panic.com/) - this project isn't affiliated with or endorsed by them in any way.
================================================
FILE: server/api.md
================================================
This is the API that is used by the Playdate console for things like fetching game updates, scoreboards, player details, etc. It is available under `https://play.date/api/v2`. All API endpoints require [auth headers](#auth-headers).
This list of endpoints was obtained by decompiling the Playdate Simulator app. Some haven't actually been seen in use, so they are only partially documented and there may be mistakes.
## Endpoints
| Method | Path |
|:-|:-|
| `POST` | [`/auth_echo/`](#post-auth_echo) |
| `GET` | [`/player/`](#get-player) |
| `GET` | [`/player/:playerId/`](#get-playerplayerid) |
| `POST` | `/player/avatar/` |
| `GET` | [`/games/scheduled/`](#get-gamesscheduled) |
| `GET` | [`/games/user/`](#get-gamesuser) |
| `GET` | `/games/testing/` |
| `GET` | [`/games/system/`](#get-gamessystem) |
| `GET` | [`/games/purchased/`](#get-gamespurchased) |
| `GET` | [`/games/catalog`](#get-gamescatalog) |
| `GET` | [`/games/catalog/:idx`](#get-gamescatalogidx) |
| `POST` | [`/games/:bundleId/purchase/`](#post-gamesbundleidpurchase) |
| `POST` | [`/games/:bundleId/purchase/confirm`](#post-gamesbundleidpurchaseconfirm) |
| `GET` | `/games/:bundleId/latest_build/` |
| `GET` | `/games/:bundleId/boards/` |
| `GET` | `/games/:bundleId/boards/:boardId/` |
| `POST` | `/games/:bundleId/boards/:boardId/` |
| `GET` | `/device/settings/` |
### POST /auth_echo
Seems to just return whatever JSON body is sent to it.
### GET /player
Returns the player profile for the user that owns the current access token.
### GET /player/:playerId
Same as `/player`, but gets the player profile for another user, given their [Player ID](#player-id).
### GET /games/scheduled
Returns an array of [Schedule](#Schedule) entries for any seasons that you have access to.
### GET /games/user
Returns an array of [Game](#Game) entries for games that you have [sideloaded](https://help.play.date/games/sideloading/).
### GET /games/system
Returns an array of [Game](#Game) entries for additional system applications, such as [Catalog](https://play.date/games/catalog/).
### GET /games/purchased
Returns an array of [Game](#Game) entries for games that you have purchased through [Catalog](https://play.date/games/catalog/).
### GET /games/catalog
Returns an array of [Catalog Game](#CatalogGame) entries for games that are available through [Catalog](https://play.date/games/catalog/).
### GET /games/catalog/:idx
Returns [Catalog Game](#CatalogGame) entry for a specific [Catalog](https://play.date/games/catalog/) game.
### POST /games/:bundleId/purchase/
Initiates the purchase flow for a game. Returns instructions for confirming the purchase on another device.
### POST /games/:bundleId/purchase/confirm
Completes the purchase flor for a game.
### GET /device/register/:serialNumber
If the device hasn't already been registered, returns a JSON containing its serial number and pin.
This endpoint requires an extra header:
| Header | Value |
|:-|:-|
| `Idempotency-Key` | Random 16-character string. Allowed chars are `0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`. |
### GET /device/register/:serialNumber/complete
Returns a JSON with the device's registered status, access token, and serial number. Access token will only be available on the first request to this endpoint after registering the device.
## Schemas
### Schedule
| Key | Type | Detail |
|:----|:------|:------|
| `name` | string | Schedule name (Season One is `Season-001`) |
| `start_date` | string | Schedule start datetime, formatted as `E MMM d HH:mm:ss yyyy zzz` (e.g. `Mon Apr 18 00:00:00 2022 PDT`) |
| `start_date_timestamp` | number | Schedule start as a UNIX timestamp |
| `next_release_timestamp` | number | Time of next scheduled release as a UNIX timestamp, can be `null` |
| `ended` | boolean | |
| `games` | array | Array of available [Game](#Game) entries |
### Game
| Key | Type | Detail |
|:----|:------|:------|
| `name` | string | Game name, will be displayed to the user |
| `bundle_id` | string | Reverse-domain formatted bundle ID (e.g `com.jaames.playnote`) |
| `short_description` | string | Few games currently have this (only seen on Flipper Lifter and Boogie Loops so far), often `null` |
| `studio` | string | Game's publisher/developer |
| `has_newer_build` | boolean | |
| `decryption_key` | string | Only present for purchased games, otherwise `null`. Unclear what this is actually used for - at the time of writing purchased games do not appear to be encrypted and the key changes on each request. Seems to be base64-encoded. |
| `latest_build` | [Build](#Build) | |
### Build
| Key | Type | Detail |
|:----|:------|:------|
| `url` | string | Web URL for the build's .zip file |
| `is_beta` | boolean | |
| `version` | string | Human-friendly version string, taken from the game's pdxinfo file |
| `build_number` | number | Incremental build number from the game's pdxinfo |
| `filesize` | number | .zip file size, in bytes |
| `upzipped_filesize` | number | Size of the .zip contents after decompression, in bytes |
### Catalog Game
| Key | Type | Detail |
|:----|:------|:------|
| `name` | string | Game name, will be displayed to the user |
| `bundle_id` | string | Reverse-domain formatted bundle ID (e.g `com.jaames.playnote`) |
| `studio` | string | Game's publisher/developer |
| `description` | string | Game's description |
| `detail_url` | string | Path for this game's [`/games/catalog/:idx`](#get-gamescatalogidx) endpoint |
| `price` | number | Price in USD |
| `header_image` | string | Path to .pdi image file |
| `list_image_size` | string | `"small"` |
| `list_image` | string | Path to .pdi image file |
| `animation_frame_duration` | unknown | Seen as `null` |
| `animation_frame_timing` | unknown | Seen as `null` |
| `accessibility` | string | Game accessibility information |
| `rating` | string | Game age rating |
| `screenshots` | [Catalog Screenshot](#CatalogScreenshot)[] | Game screenshots |
| `build_size` | string | Download size, e.g. `"22.8 MB"` |
| `published_date` | string | |
| `updated_date` | string | |
| `authorized` | boolean | |
| `purchasable` | boolean | |
| `short_description` | string | |
| `web_url` | string | URL for this game on the Catalog web storefront |
| `purchase_url` | string | Path for this game's [`/games/:bundleId/purchase/`](#post-gamesbundleidpurchase) |
### Catalog Screenshot
| Key | Type | Detail |
|:----|:------|:------|
| `url` | string | Path to .pdi image file |
| `frame_timing` | number[] | |
## Auth Headers
All routes require a basic authorization token sent via a HTTP header.
| Header | Value |
|:-|:-|
| `Authorization` | `Token ` followed by your authorization token |
### Simulator Tokens
If you have a developer account on [play.date](//play.date), you can generate a simulator-only access token by going to `https://play.date/players/account/` and clicking 'register simulator'. It looks as though you're allowed to register up to 5 simulators at one time. Note that this will be relatively limited, as the simulator cannot.
## Player ID
Player IDs seem to use the `DCE 1.1, ISO/IEC 11578:1996` variant of UUID v4, with dashes separating each section, e.g `XXXXXXXX-XXXX-4XXX-8XXX-XXXXXXXXXXXX`.
================================================
FILE: tools/pdex2elf.py
================================================
import argparse
import hashlib
import zlib
if __name__ == '__main__':
parser = argparse.ArgumentParser(
prog='pdex2elf.py',
description='Converts a Playdate pdex.bin file to an ELF file.'
)
parser.add_argument('pdex', help='the path to the input pdex.bin')
parser.add_argument('elf', help='the path to the output ELF file')
args = parser.parse_args()
with open(args.pdex, 'rb') as pdex:
pdex_magic = pdex.read(12)
if pdex_magic in [b'Playdate PDX', b'Playdate BIN']:
pdex_flags = int.from_bytes(pdex.read(4), byteorder='little')
if pdex_flags & 0x40000000:
raise ValueError('the specified pdex.bin is encrypted')
pdex_version = '2.0'
pdex_checksum = pdex.read(16)
pdex_filesz = int.from_bytes(pdex.read(4), byteorder='little')
pdex_memsz = int.from_bytes(pdex.read(4), byteorder='little')
pdex_entry = int.from_bytes(pdex.read(4), byteorder='little')
pdex_relnum = int.from_bytes(pdex.read(4), byteorder='little')
pdex_data = zlib.decompress(pdex.read())
else:
pdex_entry = int.from_bytes(pdex_magic[:4], byteorder='little') - 0x6000000c
pdex_filesz = int.from_bytes(pdex_magic[4:8], byteorder='little') - 0x6000000c
pdex_memsz = int.from_bytes(pdex_magic[8:], byteorder='little') - 0x6000000c
if pdex_entry < 0 or pdex_filesz < 0 or pdex_memsz < 0:
raise ValueError('the specified file is not a pdex.bin')
pdex_version = '1.0'
pdex_magic = None
pdex_flags = 0
pdex_relnum = 0
pdex_checksum = None
pdex_data = pdex.read()
print('pdex.bin info:')
print(' Version: {}'.format(pdex_version))
if pdex_magic is not None:
print(' File signature: {}'.format(pdex_magic.decode()))
print(' Flags: {}'.format(pdex_flags))
if pdex_checksum is not None:
print(' Declared checksum: {}'.format(pdex_checksum.hex()))
print(' Computed checksum: {}'.format(hashlib.md5(pdex_data[:pdex_filesz]).hexdigest()))
print(' Entry point: {}'.format(pdex_entry))
print(' File size: {}'.format(pdex_filesz))
print(' Memory size: {}'.format(pdex_memsz))
print(' Relocations: {}'.format(pdex_relnum))
with open(args.elf, 'wb') as elf:
text_index = 1
text_addr = 0
text_offset = 0x10000
text_size = pdex_filesz
bss_index = 2
bss_addr = (pdex_filesz + 3) & ~3
bss_offset = text_offset + bss_addr
bss_size = pdex_memsz - pdex_filesz
rel_text_index = 3
rel_text_offset = bss_offset
rel_text_size = pdex_relnum * 8
symtab_index = 4
symtab_offset = (rel_text_offset + rel_text_size + 3) & ~3
symtab_size = 2 * 16
strtab_index = 5
strtab_data = b'\0'
strtab_offset = symtab_offset + symtab_size
strtab_size = len(strtab_data)
shstrtab_index = 6
shstrtab_data = b'\0.text\0.bss\0.rel.text\0.symtab\0.strtab\0.shstrtab\0'
shstrtab_offset = strtab_offset + strtab_size
shstrtab_size = len(shstrtab_data)
sh_offset = (shstrtab_offset + shstrtab_size + 3) & ~3
# ==== ELF header ====
# e_ident[EI_MAG0..EI_MAG3]
elf.write(b'\x7fELF')
# e_ident[EI_CLASS]
elf.write(b'\x01') # ELFCLASS32
# e_ident[EI_DATA]
elf.write(b'\x01') # ELFDATA2LSB
# e_ident[EI_VERSION]
elf.write(b'\x01') # EV_CURRENT
# e_ident[EI_OSABI]
elf.write(b'\x00') # ELFOSABI_SYSV
# e_ident[EI_ABIVERSION]
elf.write(b'\x00')
# e_ident[EI_PAD..(EI_NIDENT - 1)]
elf.write(b'\x00\x00\x00\x00\x00\x00\x00')
# e_type
elf.write(b'\x02\x00') # ET_EXEC
# e_machine
elf.write(b'\x28\x00') # EM_ARM
# e_version
elf.write(b'\x01\x00\x00\x00') # EV_CURRENT
# e_entry
elf.write(pdex_entry.to_bytes(4, byteorder='little'))
# e_phoff
elf.write(b'\x34\x00\x00\x00')
# e_shoff
elf.write(sh_offset.to_bytes(4, byteorder='little'))
# e_flags
elf.write(b'\x00\x04\x00\x05') # EF_ARM_EABI_VER5 | EF_ARM_ABI_FLOAT_HARD
# e_ehsize
elf.write(b'\x34\x00')
# e_phentsize
elf.write(b'\x20\x00')
# e_phnum
elf.write(b'\x01\x00')
# e_shentsize
elf.write(b'\x28\x00')
# e_shnum
elf.write(b'\x07\x00')
# e_shstrndx
elf.write(shstrtab_index.to_bytes(2, byteorder='little'))
# ==== Program header ====
# p_type
elf.write(b'\x01\x00\x00\x00') # PT_LOAD
# p_offset
elf.write(text_offset.to_bytes(4, byteorder='little'))
# p_vaddr
elf.write(b'\x00\x00\x00\x00')
# p_paddr
elf.write(b'\x00\x00\x00\x00')
# p_filesz
elf.write(pdex_filesz.to_bytes(4, byteorder='little'))
# p_memsz
elf.write(pdex_memsz.to_bytes(4, byteorder='little'))
# p_flags
elf.write(b'\x07\x00\x00\x00') # PF_X | PF_W | PF_R
# p_align
elf.write(text_offset.to_bytes(4, byteorder='little'))
# ==== .text section ====
elf.write(b'\x00' * (text_offset - elf.tell()))
elf.write(pdex_data[:pdex_filesz])
# ==== .rel.text section ====
elf.write(b'\x00' * (rel_text_offset - elf.tell()))
for i in range(pdex_filesz, pdex_filesz + 4 * pdex_relnum, 4):
# r_offset
elf.write(pdex_data[i:(i + 4)])
# r_info
elf.write(b'\x02')
elf.write(text_index.to_bytes(2, byteorder='little'))
elf.write(b'\x00')
# ==== .symtab section ====
elf.write(b'\x00' * (symtab_offset - elf.tell()))
# NULL
# st_name
elf.write(b'\x00\x00\x00\x00')
# st_value
elf.write(b'\x00\x00\x00\x00')
# st_size
elf.write(b'\x00\x00\x00\x00')
# st_info
elf.write(b'\x00') # STB_LOCAL, STT_NOTYPE
# st_other
elf.write(b'\x00')
# st_shndx
elf.write(b'\x00\x00')
# .text
# st_name
elf.write(text_addr.to_bytes(4, byteorder='little'))
# st_value
elf.write(b'\x00\x00\x00\x00')
# st_size
elf.write(b'\x00\x00\x00\x00')
# st_info
elf.write(b'\x03') # STB_LOCAL, STT_SECTION
# st_other
elf.write(b'\x00')
# st_shndx
elf.write(text_index.to_bytes(2, byteorder='little'))
# ==== .strtab section ====
elf.write(strtab_data)
# ==== .shstrtab section ====
elf.write(shstrtab_data)
# ==== Section headers ====
elf.write(b'\x00' * (sh_offset - elf.tell()))
# NULL
# sh_name
elf.write(b'\x00\x00\x00\x00')
# sh_type
elf.write(b'\x00\x00\x00\x00') # SHT_NULL
# sh_flags
elf.write(b'\x00\x00\x00\x00')
# sh_addr
elf.write(b'\x00\x00\x00\x00')
# sh_offset
elf.write(b'\x00\x00\x00\x00')
# sh_size
elf.write(b'\x00\x00\x00\x00')
# sh_link
elf.write(b'\x00\x00\x00\x00')
# sh_info
elf.write(b'\x00\x00\x00\x00')
# sh_addralign
elf.write(b'\x00\x00\x00\x00')
# sh_entsize
elf.write(b'\x00\x00\x00\x00')
# .text
# sh_name
elf.write((shstrtab_data.index(b'\0.text\0') + 1).to_bytes(4, byteorder='little'))
# sh_type
elf.write(b'\x01\x00\x00\x00') # SHT_PROGBITS
# sh_flags
elf.write(b'\x37\x00\x00\x00') # SHF_WRITE | SHF_ALLOC | SHF_EXECINSTR | SHF_MERGE | SHF_STRINGS
# sh_addr
elf.write(text_addr.to_bytes(4, byteorder='little'))
# sh_offset
elf.write(text_offset.to_bytes(4, byteorder='little'))
# sh_size
elf.write(text_size.to_bytes(4, byteorder='little'))
# sh_link
elf.write(b'\x00\x00\x00\x00')
# sh_info
elf.write(b'\x00\x00\x00\x00')
# sh_addralign
elf.write(b'\x08\x00\x00\x00')
# sh_entsize
elf.write(b'\x00\x00\x00\x00')
# .bss
# sh_name
elf.write((shstrtab_data.index(b'\0.bss\0') + 1).to_bytes(4, byteorder='little'))
# sh_type
elf.write(b'\x08\x00\x00\x00') # SHT_NOBITS
# sh_flags
elf.write(b'\x03\x00\x00\x00') # SHF_WRITE | SHF_ALLOC
# sh_addr
elf.write(bss_addr.to_bytes(4, byteorder='little'))
# sh_offset
elf.write(bss_offset.to_bytes(4, byteorder='little'))
# sh_size
elf.write(bss_size.to_bytes(4, byteorder='little'))
# sh_link
elf.write(b'\x00\x00\x00\x00')
# sh_info
elf.write(b'\x00\x00\x00\x00')
# sh_addralign
elf.write(b'\x04\x00\x00\x00')
# sh_entsize
elf.write(b'\x00\x00\x00\x00')
# .rel.text
# sh_name
elf.write((shstrtab_data.index(b'\0.rel.text\0') + 1).to_bytes(4, byteorder='little'))
# sh_type
elf.write(b'\x09\x00\x00\x00') # SHT_REL
# sh_flags
elf.write(b'\x40\x00\x00\x00') # SHF_INFO_LINK
# sh_addr
elf.write(b'\x00\x00\x00\x00')
# sh_offset
elf.write(rel_text_offset.to_bytes(4, byteorder='little'))
# sh_size
elf.write(rel_text_size.to_bytes(4, byteorder='little'))
# sh_link
elf.write(symtab_index.to_bytes(4, byteorder='little'))
# sh_info
elf.write(text_index.to_bytes(4, byteorder='little'))
# sh_addralign
elf.write(b'\x04\x00\x00\x00')
# sh_entsize
elf.write(b'\x08\x00\x00\x00')
# .symtab
# sh_name
elf.write((shstrtab_data.index(b'\0.symtab\0') + 1).to_bytes(4, byteorder='little'))
# sh_type
elf.write(b'\x02\x00\x00\x00') # SHT_SYMTAB
# sh_flags
elf.write(b'\x00\x00\x00\x00')
# sh_addr
elf.write(b'\x00\x00\x00\x00')
# sh_offset
elf.write(symtab_offset.to_bytes(4, byteorder='little'))
# sh_size
elf.write(symtab_size.to_bytes(4, byteorder='little'))
# sh_link
elf.write(strtab_index.to_bytes(4, byteorder='little'))
# sh_info
elf.write(b'\x02\x00\x00\x00')
# sh_addralign
elf.write(b'\x04\x00\x00\x00')
# sh_entsize
elf.write(b'\x10\x00\x00\x00')
# .strtab
# sh_name
elf.write((shstrtab_data.index(b'\0.strtab\0') + 1).to_bytes(4, byteorder='little'))
# sh_type
elf.write(b'\x03\x00\x00\x00') # SHT_STRTAB
# sh_flags
elf.write(b'\x00\x00\x00\x00')
# sh_addr
elf.write(b'\x00\x00\x00\x00')
# sh_offset
elf.write(strtab_offset.to_bytes(4, byteorder='little'))
# sh_size
elf.write(strtab_size.to_bytes(4, byteorder='little'))
# sh_link
elf.write(b'\x00\x00\x00\x00')
# sh_info
elf.write(b'\x00\x00\x00\x00')
# sh_addralign
elf.write(b'\x01\x00\x00\x00')
# sh_entsize
elf.write(b'\x00\x00\x00\x00')
# .shstrtab
# sh_name
elf.write((shstrtab_data.index(b'\0.shstrtab\0') + 1).to_bytes(4, byteorder='little'))
# sh_type
elf.write(b'\x03\x00\x00\x00') # SHT_STRTAB
# sh_flags
elf.write(b'\x00\x00\x00\x00')
# sh_addr
elf.write(b'\x00\x00\x00\x00')
# sh_offset
elf.write(shstrtab_offset.to_bytes(4, byteorder='little'))
# sh_size
elf.write(shstrtab_size.to_bytes(4, byteorder='little'))
# sh_link
elf.write(b'\x00\x00\x00\x00')
# sh_info
elf.write(b'\x00\x00\x00\x00')
# sh_addralign
elf.write(b'\x01\x00\x00\x00')
# sh_entsize
elf.write(b'\x00\x00\x00\x00')
print('')
print("ELF file successfully written to '{}'".format(args.elf))
================================================
FILE: tools/pdi2png.py
================================================
#!/usr/bin/env python3
# PDI to PNG converter
# PDI docs: https://github.com/jaames/playdate-reverse-engineering/blob/main/formats/pdi.md
from struct import unpack, pack
from zlib import compress, crc32, decompress
from argparse import ArgumentParser
PDI_IDENT = b'Playdate IMG'
PNG_SIGNATURE = b'\x89PNG\r\n\x1a\n'
def png_chunk(chunk_type, data):
chunk = chunk_type + data
return pack('>I', len(data)) + chunk + pack('>I', crc32(chunk) & 0xffffffff)
def write_png(path, width, height, rows, has_alpha):
# color type: 0 = grayscale, 4 = grayscale+alpha
color_type = 4 if has_alpha else 0
ihdr = pack('>IIBBBBB', width, height, 8, color_type, 0, 0, 0)
# build raw image data: filter byte (0=none) + row pixels
raw = bytearray()
for row in rows:
raw.append(0) # filter: none
raw.extend(row)
with open(path, 'wb') as f:
f.write(PNG_SIGNATURE)
f.write(png_chunk(b'IHDR', ihdr))
f.write(png_chunk(b'IDAT', compress(bytes(raw))))
f.write(png_chunk(b'IEND', b''))
def read_cell(data, offset):
clip_width, clip_height, stride, clip_left, clip_right, clip_top, clip_bottom, flags = \
unpack('<8H', data[offset:offset + 16])
offset += 16
has_alpha = (flags & 0x3) > 0
# read color bitmap (1-bit, 0=black 1=white)
color_size = stride * clip_height
color_data = data[offset:offset + color_size]
offset += color_size
# read alpha bitmap if present (1-bit, 0=transparent 1=opaque)
alpha_data = None
if has_alpha:
alpha_data = data[offset:offset + color_size]
offset += color_size
# reconstruct full image dimensions
full_width = clip_left + clip_width + clip_right
full_height = clip_top + clip_height + clip_bottom
# build pixel rows
rows = []
for y in range(full_height):
if has_alpha:
row = bytearray(full_width * 2) # grayscale + alpha per pixel
else:
row = bytearray(b'\xff' * full_width) # default white
cy = y - clip_top
if 0 <= cy < clip_height:
row_offset = cy * stride
for x in range(clip_width):
byte_index = row_offset + (x // 8)
bit_index = 7 - (x % 8)
color_bit = (color_data[byte_index] >> bit_index) & 1
color = 255 if color_bit else 0
px = clip_left + x
if has_alpha:
alpha_bit = (alpha_data[byte_index] >> bit_index) & 1
alpha = 255 if alpha_bit else 0
row[px * 2] = color
row[px * 2 + 1] = alpha
else:
row[px] = color
rows.append(row)
return full_width, full_height, rows, has_alpha
def convert_pdi(input_path, output_path):
with open(input_path, 'rb') as f:
data = f.read()
# verify ident
ident = data[0:12]
if ident != PDI_IDENT:
raise ValueError(f'Not a valid PDI file (got ident {ident!r})')
flags = unpack('<I', data[12:16])[0]
is_compressed = (flags & 0x80000000) > 0
offset = 16
if is_compressed:
decompressed_size, width, height, reserved = unpack('<4I', data[offset:offset + 16])
offset += 16
image_data = decompress(data[offset:])
else:
image_data = data[offset:]
full_width, full_height, rows, has_alpha = read_cell(image_data, 0)
write_png(output_path, full_width, full_height, rows, has_alpha)
print(f'Saved {output_path} ({full_width}x{full_height})')
if __name__ == '__main__':
parser = ArgumentParser(description='Convert Playdate .pdi images to .png')
parser.add_argument('input', help='Input .pdi file path')
parser.add_argument('-o', '--output', help='Output .png file path (default: input with .png extension)')
args = parser.parse_args()
output = args.output
if not output:
if args.input.lower().endswith('.pdi'):
output = args.input[:-4] + '.png'
else:
output = args.input + '.png'
convert_pdi(args.input, output)
================================================
FILE: tools/pdz.py
================================================
# based on https://gist.github.com/zhuowei/666c7e6d21d842dbb8b723e96164d9c3
# PDZ docs: https://github.com/jaames/playdate-reverse-engineering/blob/main/formats/pdz.md
from sys import exit
from os import path, makedirs
from struct import pack, unpack
from zlib import decompress
from argparse import ArgumentParser
PDZ_IDENT = b'Playdate PDZ'
FILE_TYPES = {
1: 'luac',
2: 'pdi',
3: 'pdt',
4: 'pdv',
5: 'pda',
6: 'pds',
7: 'pft',
}
FILE_IDENTS = {
'pdi': b'Playdate IMG',
'pdt': b'Playdate IMT',
'pdv': b'Playdate VID',
'pda': b'Playdate AUD',
'pds': b'Playdate STR',
'pft': b'Playdate FNT'
}
class PlaydatePdz:
@classmethod
def open(cls, path):
with open(path, "rb") as buffer:
return cls(buffer)
def __init__(self, buffer):
self.buffer = buffer
self.entries = {}
self.num_entries = 0
self.read_header()
self.read_entries()
def read_header(self):
self.buffer.seek(0)
magic = self.buffer.read(16)
magic = magic[:magic.index(b'\0')] # trim null bytes
assert magic == PDZ_IDENT, 'Invalid PDZ file ident'
self.buffer.seek(12)
flags = unpack('<I', self.buffer.read(4))[0]
is_encrypted = (flags & 0x40000000) > 0
assert not is_encrypted, 'PDZ file is encrypted'
def read_string(self):
res = b''
while True:
char = self.buffer.read(1)
if char == b'\0': break
res += char
return res.decode()
def read_entries(self):
self.buffer.seek(0, 2)
ptr = 0x10
pdz_len = self.buffer.tell()
self.buffer.seek(ptr)
while ptr < pdz_len:
head = unpack('<I', self.buffer.read(4))[0]
flags = head & 0xFF
entry_len = (head >> 8) & 0xFFFFFF
# doesn't seem to be any other flags
is_compressed = (flags >> 7) & 0x1
file_type = FILE_TYPES[flags & 0xF]
# file name is a null terminated string
file_name = self.read_string()
# align offset to next nearest multiple of 4
self.buffer.seek((self.buffer.tell() + 3) & ~3)
# .pda files have two more values after filename before data
if file_type == 'pda':
entry_len -= 4
audio_info = unpack('<I', self.buffer.read(4))[0]
audio_rate = audio_info & 0xFFFFFF
audio_format = (audio_info >> 24) & 0xFF
# if compression flag is set, there's another uint32 with the decompressed size
if is_compressed:
decompressed_size = unpack('<I', self.buffer.read(4))[0]
entry_len -= 4
else:
decompressed_size = entry_len
data = self.buffer.read(entry_len)
ptr = self.buffer.tell()
self.num_entries += 1
self.entries[file_name] = {
'name': file_name,
'type': file_type,
'data': data,
'size': entry_len,
'compressed': is_compressed,
'decompressed_size': decompressed_size
}
if file_type == 'pda':
self.entries[file_name].update({
'audio_rate': audio_rate,
'audio_format': audio_format})
def get_entry_data(self, name):
assert name in self.entries
entry = self.entries[name]
if entry['compressed']:
return decompress(entry['data'])
return entry['data']
def construct_entry_header(self, name):
# this is probably incorrect, use at your own risk
assert name in self.entries
entry = self.entries[name]
file_type = entry['type']
is_compressed = entry['compressed']
assert file_type in ['pdi','pdt','pdv','pda','pds','pft']
ident = FILE_IDENTS[file_type]
if file_type == 'pda':
rate = entry['audio_rate']
fmt = entry['audio_format']
audio_info = (fmt << 24) + rate
header = pack('<12sI', ident, audio_info)
else:
flags = 0x80000000 if is_compressed else 0x00000000
header = pack('<12sI', ident, flags)
return header
def save_entry_data(self, name, outdir, gen_header):
assert name in self.entries
print(f'processing entry: {name}')
entry = self.entries[name]
file_type = entry['type']
data = self.get_entry_data(name)
filepath = outdir + '/' + entry['name'] + '.' + entry['type']
if '/' in filepath:
makedirs(path.dirname(filepath), exist_ok=True)
with open(filepath, 'wb') as outfile:
if gen_header and file_type in ['pdi','pdt','pdv','pda','pds','pft']:
hdr = self.construct_entry_header(name)
outfile.write(hdr)
outfile.write(data)
def save_entries(self, outdir, gen_header):
for name in self.entries:
self.save_entry_data(name, outdir, gen_header)
def print_entries(self):
for name in self.entries:
print(f'{name}: {self.entries[name]["type"]}')
if __name__ == "__main__":
parser = ArgumentParser(prog="pdz.py", description="Extract contents of a pdz file.")
parser.add_argument("-o", "--outdir", default="pdz_output", help="output directory", dest="out_dir")
parser.add_argument("-i", "--infile", help="input file", dest="in_file", required=True)
parser.add_argument("-l", "--list-files", help="print a list of all entries in the file, ignoring all other arguments",
dest="list_files", required=False, action="store_true")
parser.add_argument("-g", "--gen-headers", help="generate file headers for pd* files (experimental, default=false)" ,
dest="gen_headers", required=False, action="store_true")
parser.add_argument("-f", "--extract-file", help="extract the given file(s), or all if this arg isn't provided",
dest="file_list", required=False, action="append")
args = parser.parse_args()
pdz = PlaydatePdz.open(args.in_file)
if args.list_files:
pdz.print_entries()
exit()
if args.file_list:
for f in args.file_list:
pdz.save_entry_data(f, args.out_dir, args.gen_headers)
else:
pdz.save_entries(args.out_dir, args.gen_headers)
================================================
FILE: tools/usbeval.py
================================================
if (len(argv) < 2):
print('usbeval.py')
print('Evaluates a Lua script on a Playdate device, via USB')
print('Requires pdc from the Playdate SDK as well as the pyusb library')
print('Usage:')
print('python3 usbeval.py ./input.lua')
exit()
import tempfile
import subprocess
import usb.core
import usb.util
from sys import argv
from pathlib import Path
from struct import unpack
from zlib import decompress
from time import sleep
# Playdate USB vendor and product IDs
PLAYDATE_VID = 0x1331;
PLAYDATE_PID = 0x5740;
IN_SIZE = 64;
def pdz_extract_entry(data, entry):
ptr = 0x10
while ptr < len(data):
flags = data[ptr]
is_compressed = (flags >> 7) & 0x1
innerlen = data[ptr + 1] | (data[ptr + 2] << 8)
filename = data[ptr + 4 : data.find(b"\0", ptr + 4)]
outerheadersize = 4 + len(filename) + 1
outerheadersize = ((ptr + outerheadersize + 3) & ~3) - ptr
zlibdata = data[ptr + outerheadersize + 4: ptr + outerheadersize + innerlen]
if filename.decode('utf-8') == entry:
return decompress(zlibdata)
ptr += outerheadersize + innerlen
return None
def usb_connect():
# find our playdate device
device = usb.core.find(idVendor=PLAYDATE_VID, idProduct=PLAYDATE_PID)
if device is None:
raise ValueError('Device not found')
# set the active configuration. With no arguments, the first
# configuration will be the active one
device.set_configuration()
# get an endpoint instance
cfg = device.get_active_configuration()
intf = cfg[(1,0)]
epOut = usb.util.find_descriptor(
intf,
# match the first OUT endpoint
custom_match = \
lambda e: \
usb.util.endpoint_direction(e.bEndpointAddress) == \
usb.util.ENDPOINT_OUT)
epIn = usb.util.find_descriptor(
intf,
# match the first IN endpoint
custom_match = \
lambda e: \
usb.util.endpoint_direction(e.bEndpointAddress) == \
usb.util.ENDPOINT_IN)
assert epOut is not None
assert epIn is not None
device.reset()
return epOut, epIn
def usb_read_bytes(endPoint):
res = bytearray()
has_started = False
while True:
try:
b = bytearray(epIn.read(IN_SIZE))
res += b
if b != b'': has_started = True
if has_started and b == b'': break
except usb.core.USBTimeoutError:
break
return res
with tempfile.NamedTemporaryFile(prefix='main', suffix='.lua') as luafile, tempfile.TemporaryDirectory(suffix='.pdx') as pdxdir:
# copy lua file
print('reading input file')
with open(argv[1], 'rb') as infile:
luafile.write(infile.read())
luafile.seek(0)
# compile lua with pdc
print('compiling lua with pdc')
subprocess.run(['pdc', luafile.name, pdxdir])
luastem = Path(luafile.name).stem
# extract lua bytecode from pdz
print('extracting lua bytecode')
with open(Path(pdxdir, luastem + '.pdz'), 'rb') as pdzfile:
pdz = pdzfile.read()
bytecode = pdz_extract_entry(pdz, luastem)
# connect to playdate over usb
print('finding playdate connected to usb...')
epOut, epIn = usb_connect()
print('successfully connected to playdate!')
# set usb echo mode to off
print('setting usb echo to off')
epOut.write('echo off\n')
resp = usb_read_bytes(epIn)
# get version info (to test things work, but also looks cool :^))
# print('playdate version info:')
# epOut.write('version\n')
# resp = usb_read_bytes(epIn)
# print(resp.decode("utf-8").strip())
# consume printed console content until there's nothing new
print('clearing current console data')
epOut.write('eval\n')
sleep(.2)
usb_read_bytes(epIn)
# send lua bytecode to the device
print('sending payload for device to eval...')
header = b'eval %d\n' % len(bytecode)
payload = header + bytecode
epOut.write(payload)
sleep(.2) # payload seems to take a bit to execute, you may need to adjust this if you have a big payload
resp = usb_read_bytes(epIn)
print('===============')
print('console output:')
print(resp.decode("utf-8").strip())
# keep polling for new console output
while True:
try:
sleep(.1)
resp = usb_read_bytes(epIn)
text = resp.decode("utf-8").strip()
if text: print(text)
except KeyboardInterrupt:
break
================================================
FILE: usb/stream.md
================================================
# The `stream` protocol
When `stream enable` is sent over serial, the Playdate enters streaming mode, used by Playdate Mirror.
In this mode, the device continuously reports the state of its screen, audio engine, buttons, and crank over serial, using a special protocol to do so.
(As with all Playdate formats, all numbers reported using the protocol are little-endian.)
### Entering, exiting, and maintaining streaming mode
If the Playdate has been in streaming mode for over one second without a `stream poke` command sent over USB, it will stop reporting data.
However, it won't completely exit streaming mode -- any future `stream enable` will *not* resend the entire screen afterward.
To exit streaming mode, send `stream disable` via serial.
### Stream messages
While the Playdate is streaming, it will report data in short messages.
The general format of one of these messages is:
| **Offset** | **Data type** | **Content** |
|:-----|:-----|:-----|
| 0 | `uint16` | Message type (see below) |
| 2 | `uint16` | Payload length in bytes |
The actual payload data follows this 4-byte header.
### Audio control
To control how audio is reported, there are three options you can send during streaming mode.
(The format the samples are parsed with isn't actually changed until the format switch is acknowledged with a stream message!)
| **Command** | **Meaning** |
|:-----|:-----|
| `stream a+` | Switch to stereo signed 16-bit PCM audio |
| `stream am` | Switch to mono signed 16-bit PCM audio |
| `stream a-` | Don't send any audio (the default) |
## Message types
### `0x0001`: Input state
Reports the state of the Playdate's buttons and crank every frame.
| **Payload offset** | **Data type** | **Content** |
|:-----|:-----|:-----|
| 0 | `uint16` | Button flags (see below) |
| 2 | `uint16` | Unknown: Seems to change erratically |
| 4 | `float` | Crank angle in degrees |
#### Button flags
| **Bitmask** | **Meaning** |
|:-----|:-----|
| `flags & 0x0001` | If `> 0`, d-pad left button is pressed |
| `flags & 0x0002` | If `> 0`, d-pad right button is pressed |
| `flags & 0x0004` | If `> 0`, d-pad up button is pressed |
| `flags & 0x0008` | If `> 0`, d-pad down button is pressed |
| `flags & 0x0010` | If `> 0`, B button is pressed |
| `flags & 0x0020` | If `> 0`, A button is pressed |
| `flags & 0x0040` | If `> 0`, Menu button is pressed |
### `0x000A`: New frame (no delay)
Starts a new frame as fast as possible, with no delay from the previous frame.
### `0x000B`: End frame
Ends the current frame. In the official Mirror app, this flips the framebuffer to make it visible on-screen.
### `0x000C`: Update screen line
Signals that a single horizontal line of the display was updated.
Only the lines that have changed since the last frame are sent, unless the current frame is the first one.
In that case, the entire screen is sent as 240 line update messages.
| **Payload offset** | **Data type** | **Content** |
|:-----|:-----|:-----|
| 0 | `uint8` | Line number that was updated (starting at one and bit-reversed, so line 0 becomes `0b10000000 == 0x80`) |
| 1 | `50 bytes` | Line data, left to right, with the least significant bit coming first in each byte |
| 51 | `uint8` | Zero byte to pad the length to a multiple of 4 bytes |
### `0x000D`: New frame (with delay)
Starts a new frame a certain amount of time since the previous frame started.
| **Payload offset** | **Data type** | **Content** |
|:-----|:-----|:-----|
| 0 | `uint32` | Milliseconds since the start of the previous frame |
### `0x0014`: Audio frames (multiple)
Sends one or multiple audio frames to buffer for playback.
The data format is signed 16-bit PCM, with the left channel of each sample coming first.
(If the audio format is mono, then the right channel will be zero.)
### `0x0015`: Audio format switch acknowledge
Signals that a requested change in audio formats has taken place.
| **Payload offset** | **Data type** | **Content** |
|:-----|:-----|:-----|
| 0 | `uint16` | Audio format flags (see below) |
#### Audio format flags
| **Bitmask** | **Meaning** |
|:-----|:-----|
| `flags & 0x0001` | If `> 0`, audio is enabled |
| `flags & 0x0002` | If `> 0`, audio has two channels |
### `0x0016`: Audio frames (fill single)
Sends one audio sample to continuously play until more data is received. This usually denotes silence.
Like with type `0x0014`, the data format is signed 16-bit PCM, with the left channel of the sample coming first.
================================================
FILE: usb/usb.md
================================================
When connected to USB and unlocked, the Playdate provides a serial interface over USB. Commands are sent as ascii and must end in a newline character (`\n`). If the currently running game logs something to the console (e.g. via `print()` in Lua, or `playdate->system->logToConsole` in C) this will also be sent via serial output. Some commands (such as `button` or `stream enable`) will cause the Playdate to continually send data until another command is sent to cancel it, other commands (for example, `bitmap`) may require extra binary data to be sent after the newline character.
Some of these commands are used by Playdate Simulator for features like "preview bitmap" or "run pdx", or to enable streaming in Playdate mirror. If you want to play around with these, and you use a browser that supports WebSerial, you should check out my [pd-usb](https://github.com/jaames/pd-usb) library.
## USB details
|||
|:----------------|:------|
| *USB Vendor ID* | `0x1331` |
| *USB Product ID* | `0x5740` |
| *Baud rate* | `115200` |
## Connecting to the USB
You can use any terminal emulator to connect to the virtual serial port. For example, on macOS, you can connect using picocom by running:
`picocom -b 115200 -p n -d 8 --omap crcrlf /dev/cu.usbmodemPD<serial number>`
When you're done, disconnect and quit picocom with `CTRL-a` `CTRL-x`
## USB commands
Running `help` will return a very helpful list of available commands:
```
The following commands are available:
Telnet commands:
help Displays all available commands or individual help on each command
CPU Control:
serialread Print the device serial number
trace trace_<delay>. (trace 10)
stoptrace stoptrace
bootdisk reboot into recovery segment USB disk
datadisk reboot into data segment USB disk
factoryreset factory reset
formatdata format data disk
settime sets the RTC. format is ISO8601 plus weekday (1=mon) e.g.: 2018-03-20T19:58:29Z 2
gettime reads the RTC
vbat get battery voltage
rawvbat get raw battery adc value
batpct get battery percentage
temp get estimated ambient temperature
dcache dcache <on/off>: turn dcache on or off
icache icache <on/off>: turn icache on or off
Runtime control:
echo echo (on|off): turn console echo on or off
buttons Test buttons & crank
tunebuttons tunebuttons <debounce> <holdoff>
btn btn <btn>: simulate a button press. +a/-a/a for down/up/both
changecrank changecrank +-<degrees>
dockcrank simulates crank docking
enablecrank Reenables crank updates
disablecrank Disables crank updates
accel simulate accelerometer change
screen Dump framebuffer data (400x240 bits)
bitmap Send bitmap to screen (followed by 400x240 bits)
controller start or stop controller mode
eval execute a compiled Lua function
run run <path to pdx>: Run the named program
luatrace Get a Lua stack trace
stats Display runtime stats
autolock autolock <always|onBattery>
version Display build target and SDK version
memstats memstats
hibernate hibernate
Stream:
stream stream <enable|disable|poke>
ESP functions:
espreset reset the ESP chip
espoff turn off the ESP
espbootlog get the ESP startup log
espfile espfile <path> <address> <md5> <uncompressed size>: add the given file to the upload list. If <uncompressed size> is added then the file is assumed to be compressed.
espflash espflash <baud> [0|1] send the files listed with the espfile command to the ESP flash.
espbaud espbaud <speed> [cts]
esp esp <cmd>: Forward a command to the ESP firmware, read until keypress
Firmware Update:
fwup fwup [bundle_path]
```
Secret commands:
```
formatboot
unlock
islocked
```
Most of these commands are self-explanatory, so I will just detail some of the interesting/different ones. Please note these were run on an older Playdate Developer Preview unit, so there may be some differences with the final units that get shipped to the public.
### `version`
Example output:
```
~version:
target=DVT1
build=c4abdb37253e-1.7.0-release.127473-buildbot-20211215_200649
boot_build=c4abdb37253e-1.7.0-release.127473-buildbot
SDK=1.7.0
pdxversion=10500
serial#=<REDACTED>
cc=9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599]
```
### `stats`
Example output:
```
~stats:
frame count: 194503
frame time: 0.000977
gc time: 0.016602
disp time: 18
current time: 9855691
mem alloced: 403288
mem reserved: 460448
mem total: 16645684
kernel: 0.1%
serial: 0.0%
game: 2.5%
GC: 35.2%
wifi: 0.0%
trace: 0.0%
audio: 0.2%
```
### `buttons`
Begins button-testing mode causes the device to begin continually writing the current control state to USB bulk in, at approximately 50 times per second. This can be stopped by sending a newline to the device.
Each new state will be written as a single line with the following structure:
```
buttons:XX XX XX crank:X.X docked:X
```
`button` gives three hex-formatted numbers containing the current button state. The first number indicates which buttons are currently pressed, the second indicates which buttons were pressed after the last update, and the third indicates which buttons were released after the last update. These should be treated as bitflags:
| Button | Bitmask |
|:-------|:--------|
| a | `0x01` |
| b | `0x02` |
| up | `0x04` |
| down | `0x08` |
| left | `0x10` |
| right | `0x20` |
| menu | `0x40` |
| lock | `0x80` |
`crank` gives the crank angle as a floating point number, measured in degrees, with `0` being the 12 o'clock position.
`docked` will be `0` if the crank is not docked, or `1` if docked.
### `screen`
Gets the current screen buffer as a 1-bit array of pixels. The data returned from the Playdate will begin with an 11-byte string `\r\nscreen~:\n`, followed by 12000 bytes of bitmap data where each bit of every byte represents one pixel; `0` for black, `1` for white.
### `bitmap`
Sends a 1-bit bitmap to be previewed on the Playdate screen. The command must begin with a 7-byte command string `bitmap\n`, followed by 12000 bytes of bitmap data where each bit of every byte represents one pixel; `0` for black, `1` for white.
### `run`
Launches a .pdx rom from the Playdate's data partition. The game path must begin with a forward slash, e.g `run /System/Crayons.pdx`.
### `eval`
Evaluates a compiled Lua function on the device. The command must begin with the string `eval %d\n` where `%d` is the length of the data to eval. This should then be followed by the data for a compiled Lua function. You can use the `usbeval.py` script in this repo's `tools` directory to play around with this.
*Note: `eval` doesn't work in System apps (Launcher, Settings, etc) nor games purchased from the Catalog. You must remove `hash` from the purchased game in order to allow for eval.*
This command will not work if the currently loaded game is from the System directory on the device, presumably for security reasons.
### `stream`
Used for interacting with the Playdate's [video/audio streaming protocol](/usb/stream.md), as used by Playdate Mirror.
### `esp`
Using the `esp <cmd>` command will forward an [ESP-AT](https://docs.espressif.com/projects/esp-at/en/latest/Get_Started/What_is_ESP-AT.html) command to the ESP32 firmware. Please note that these commands can potentially be very dangerous and may even damage your Playdate, so only mess with this stuff if you're stupid (me) or know what you're doing (not me). In the event that you do goof something up, you may be able to recover by holding down the Playdate's secret power/reset button, which is hidden in the cavity where the crank handle goes when it's docked. Have paperclips on standby!
After sending an ESP-AT command, you need to continue reading from the device until you receive a line that contains `OK` or `ERROR` to get the full response.
I can't profess to be very experienced here, so I didn't poke to deeply. However here's the results of some of the commands I tried:
[**`AT+GMR`**](https://docs.espressif.com/projects/esp-at/en/latest/AT_Command_Set/Basic_AT_Commands.html#at-gmr-check-version-information):
Version information:
```
AT version:2.0.0.0-dev(b6850a4 - Oct 24 2019 12:10:13)
SDK version:v3.3-beta3-170-g91f29bef17
compile time(e9c8abb):Dec 15 2021 20:08:07
```
[**`AT+CMD?`**](https://docs.espressif.com/projects/esp-at/en/latest/AT_Command_Set/Basic_AT_Commands.html#at-cmd-list-all-at-commands-and-types-supported-in-current-firmware):
Querying supported commands doesn't seem to be supported.
[**`AT+UART_CUR?`**](https://docs.espressif.com/projects/esp-at/en/latest/AT_Command_Set/Basic_AT_Commands.html#at-uart-cur-current-uart-configuration-not-saved-in-flash):
Current UART configuration:
```
+UART_CUR:2534653,8,1,0,3
```
[**`AT+SYSFLASH?`**](https://docs.espressif.com/projects/esp-at/en/latest/AT_Command_Set/Basic_AT_Commands.html#at-sysflash-query-set-user-partitions-in-flash):
Querying user partitions in ESP flash:
```
AT+SYSFLASH?
+SYSFLASH:"ble_data",64,1,0x21000,0x3000
+SYSFLASH:"server_cert",64,2,0x24000,0x2000
+SYSFLASH:"server_key",64,3,0x26000,0x2000
+SYSFLASH:"server_ca",64,4,0x28000,0x2000
+SYSFLASH:"client_cert",64,5,0x2a000,0x2000
+SYSFLASH:"client_key",64,6,0x2c000,0x2000
+SYSFLASH:"client_ca",64,7,0x2e000,0x2000
+SYSFLASH:"factory_param",64,8,0x30000,0x1000
+SYSFLASH:"wpa2_cert",64,9,0x31000,0x2000
+SYSFLASH:"wpa2_key",64,10,0x33000,0x2000
+SYSFLASH:"wpa2_ca",64,11,0x35000,0x2000
+SYSFLASH:"mqtt_cert",64,12,0x37000,0x2000
+SYSFLASH:"mqtt_key",64,13,0x39000,0x2000
+SYSFLASH:"mqtt_ca",64,14,0x3b000,0x2000
+SYSFLASH:"fatfs",1,129,0x70000,0x90000
```
Most partitions don't seem to be readable, however you can for example dump the client CA cert by doing `AT+SYSFLASH=2,"client_ca",0,0x2000`.
[**`AT_FS`**](https://docs.espressif.com/projects/esp-at/en/latest/AT_Command_Set/Basic_AT_Commands.html#esp32-only-at-fs-filesystem-operations):
No file system commands seem to be supported.
### `btn`
The `btn` command can also send key presses and releases, which call the associated callbacks for the current game.
Most keys on the computer keyboard have a single-byte representation in the `btn` command. In the case of printable characters, this corresponds to their ASCII codepoint, but other keys like arrows and function keys also have a number used by the Simulator.
You can find a full set of the key codes used by the Simulator [here](https://gist.github.com/scratchminer/dcbf3410a7e72151ea68273adce9c932).
For a key press event, send `btn vK`, where `K` is the key code byte.
For a key release event, send `btn ^K`, again where `K` is the key code byte.
### `unlock`
Takes a 32 character unlock code and compares it to the device's unlock code.
If it matches, [additional commands](usb_unlocked.md) are enabled.
The unlock code is likely unique to each device and is located in different memory regions depending
on the playdate hardware revision.
- As of firmware 1.10, HW revision A contains the key at address `0x1FF0F040`.
- HW revision B contains the key at address `0x08FFF040`, located in the OTP region of the flash memory.
The following code snippet may be used to dump your key. Use the memory location that corresponds to
the HW revision you have. Even though the memory region is protected in unprivileged mode, in firmware
2.4.2, the write succeeds and then the console crashes. You may then find the key in the data drive.
```c
SDFile* file = pd->file->open("unlockkey.txt", kFileWrite);
pd->file->write(file, (const char*)0x1FF0F040, 0x20);
pd->file->close(file);
```
### `islocked`
Prints `1` if the serial console is locked, or `0` if the device successfully ran `unlock`.
## Changes
### 2.0
- Added `rawvbat` command
### 1.12.3
(these commands were observed in 1.12.3, but may have been introduced earlier)
- Added `factoryreset` command
- Added `tunebuttons` command
- Changed `autolock` to remove never option.
### 1.7.0
- Added the `hibernate` command.
- `version` output:
```
~version:
target=DVT1
build=c4abdb37253e-1.7.0-release.127473-buildbot-20211215_200649
boot_build=c4abdb37253e-1.7.0-release.127473-buildbot
SDK=1.7.0
pdxversion=10500
serial#=<REDACTED>
cc=9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599]
```
### 1.4.0
Initial version tested
================================================
FILE: usb/usb_unlocked.md
================================================
## USB commands in unlocked mode
This is the output of running `help` on a Playdate with firmware 1.10 after running `unlock`:
```
The following commands are available:
Telnet commands:
help Displays all available commands or individual help on each command
loop loop next command x times with delay y ms (eg loop 10 100 help)
lock Lock the firmware command interface
eMMC kvstore:
kvget kvget <key> [len]
kvput kvput <key> <len>
kvrm kvrm <key>
kvwipe kvwipe
CPU Control:
gpio gpio <port> <i(pu/pd)/1(pu/pd)/0(pu/pd)>. (gpio A3 1 or gpio B2 ipu)
i2cread i2cread <unit> <address> <register> <len>. (i2cread 0 0x47 0x38 3)
i2cwrite i2cwrite <unit> <address> <register> <value>. (i2cwrite 3 0x47 0x38 0xff)
tlv493read tlv493read <address> <len>. (tlv493read 0x47 3)
tlv493write tlv493write <address> <val>. (tlv493write 0x47 0x00 0x37 0x48)
serialwrite serialwrite <string>
serialread Print the device serial number
pwwrite pwwrite <string>
pwread pwread
reset reset system
dfu reset system
trace trace_<delay>. (trace 10)
stoptrace stoptrace
bootdisk reboot into recovery segment USB disk
datadisk reboot into data segment USB disk
sysdisk reboot into system segment USB disk
factoryreset factory reset
formatboot format recovery disk
formatdata format data disk
formatsys format system disk
shutdown put device in stop mode
cleartime clear time from device
settime sets the RTC. format is ISO8601 plus weekday (1=mon) e.g.: 2018-03-20T19:58:29Z 2
gettime reads the RTC
rtccalib rtccalib <on/off>: enable rtc calibration output
vbat get battery voltage
batpct get battery percentage
temp get estimated ambient temperature
peek peek <addr>: Read a 32-bit value from memory
poke poke <addr> <value>: Write a 32-bit value int memory
dump dump <start> <end>: Dump memory in range [start-end) as bytes
dumpw dumpw <start> <end>: Dump memory in range [start-end) as words
stop stop cpu
led led <R> <G> <B>. (led 0-255 0-255 0-255)
leddemo leddemo
charge charge <on/off>: enables/disables battery charging
readcharger readcharger
burn waste cpu to run down the battery
lsedrive get LSE drive level
extv get 5V_ext voltage
dcache dcache <on/off>: turn dcache on or off
icache icache <on/off>: turn icache on or off
rcccsr report bootinfo.rcccsr value
rdp rdp <enable/disable>
suspendusb sets USB current limit to 500uA
eMMC Control:
emmctest emmctest
emmcinfo emmcinfo
emmcwipe emmcwipe
emmcdump emmcdump <addr> <len>
Audio Control:
audiotest Send test data to audio output
audiosweep audiosweep <startfreq> <endfreq> <length>: Send sweep signal to audio output. Frequencies are (integer) Hz, length is milliseconds
stopaudio Stop audio output
startaudio Start audio output
audioout audioout <speaker|headphone|both|none|bt|onboard>
mictest mictest <int|ext> <filename> <length>: Record microphone to given file. length is in milliseconds
blowtest blowtest
micbiastest micbiastest <level:0-5> <verbose:0/1>
Encryption:
encrypt encrypt <file_in> <file_out>
decrypt decrypt <file_in> <file_out>
Runtime control:
echo echo (on|off): turn console echo on or off
buttons Test buttons & crank
tunebuttons tunebuttons <debounce> <holdoff>
btn btn <btn>: simulate a button press. +a/-a/a for down/up/both
changecrank changecrank +-<degrees>
dockcrank simulates crank docking
enablecrank Reenables crank updates
disablecrank Disables crank updates
accel simulate accelerometer change
pause Pause execution
resume Resume execution
restart Restart Lua runtime
step Step one frame
fps fps to return current frame rate, fps <frame rate> to set
screen Dump framebuffer data (400x240 bits)
bitmap Send bitmap to screen (followed by 400x240 bits)
lcdtest Draw a marching stripes pattern on the screen
controller start or stop controller mode
eval execute a compiled Lua function
run run <path to pdx>: Run the named program
whatsrunning Returns the path of the currently running program
luatrace Get a Lua stack trace
stats Display runtime stats
autolock autolock <always|onBattery>
version Display build target and SDK version
station station <travel agent station name>: Configure device for running at the named station
setvolume setvolume <amt>:, 0-255
getvolume Returns the volume level 0-255
wifi wifi <GET|POST> <url> [file1] [file2]
wifitest wifitest <network> <password>
memtest memtest
memstats memstats
woprset woprset [server]
woprget woprget
syncperiod syncperiod <initial_s> [period_s]
hibernate hibernate
Filesystem Stuff:
listfiles listfiles path: list files at path
getfile getfile <path>: get file contents at path
putfile putfile <path> <size>: upload file to path
mkdir mkdir <path>
delete delete <path>
rmdir rmdir <path>: Recursively delete directory
unzip unzip zip_path out_path
md5 md5 <file_path>
memio commands:
memiomd5 memiomd5 <addr> <size>
Stream:
stream stream <enable|disable|poke>
ESP functions:
espreset reset the ESP chip
espoff turn off the ESP
espbootlog get the ESP startup log
espfile espfile <path> <address> <md5> <uncompressed size>: add the given file to the upload list. If <uncompressed size> is added then the file is assumed to be compressed.
espflash espflash <baud> [0|1] send the files listed with the espfile command to the ESP flash.
espbaud espbaud <speed> [cts]
esp esp <cmd>: Forward a command to the ESP firmware, read until keypress
esptest esptest <baud> <cts>
espwipe espwipe
espversion espversion
wifiperf wifiperf <addr> <port>
wifistop wifistop
Firmware Update:
fw fw
fwup fwup [bundle_path]
recovery recovery
unstage unstage
unzip test:
unzipmem unzipmem <addr> <len> <path>
Memfault Tests:
hardfault Trigger a hardfault
assert Trigger an assert
mflt mflt <status|clear|send>
```
## Changes
### 1.12.x
- Added `factoryreset`
- Added `tunebuttons`
- Added `never` option for `autolock`
gitextract_ijazx7lw/
├── .github/
│ └── FUNDING.yml
├── .gitignore
├── LICENSE
├── formats/
│ ├── fnt.md
│ ├── luac.md
│ ├── pda.md
│ ├── pdex.md
│ ├── pdi.md
│ ├── pds.md
│ ├── pdt.md
│ ├── pdv.md
│ ├── pdz.md
│ └── pft.md
├── readme.md
├── server/
│ └── api.md
├── tools/
│ ├── pdex2elf.py
│ ├── pdi2png.py
│ ├── pdz.py
│ └── usbeval.py
└── usb/
├── stream.md
├── usb.md
└── usb_unlocked.md
SYMBOL INDEX (18 symbols across 3 files)
FILE: tools/pdi2png.py
function png_chunk (line 12) | def png_chunk(chunk_type, data):
function write_png (line 16) | def write_png(path, width, height, rows, has_alpha):
function read_cell (line 33) | def read_cell(data, offset):
function convert_pdi (line 85) | def convert_pdi(input_path, output_path):
FILE: tools/pdz.py
class PlaydatePdz (line 29) | class PlaydatePdz:
method open (line 31) | def open(cls, path):
method __init__ (line 35) | def __init__(self, buffer):
method read_header (line 42) | def read_header(self):
method read_string (line 52) | def read_string(self):
method read_entries (line 60) | def read_entries(self):
method get_entry_data (line 106) | def get_entry_data(self, name):
method construct_entry_header (line 113) | def construct_entry_header(self, name):
method save_entry_data (line 131) | def save_entry_data(self, name, outdir, gen_header):
method save_entries (line 146) | def save_entries(self, outdir, gen_header):
method print_entries (line 150) | def print_entries(self):
FILE: tools/usbeval.py
function pdz_extract_entry (line 24) | def pdz_extract_entry(data, entry):
function usb_connect (line 39) | def usb_connect():
function usb_read_bytes (line 74) | def usb_read_bytes(endPoint):
Condensed preview — 22 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (100K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 62,
"preview": "# These are supported funding model platforms\n\ngithub: jaames\n"
},
{
"path": ".gitignore",
"chars": 28,
"preview": "testing\ntools/*lua\n.obsidian"
},
{
"path": "LICENSE",
"chars": 7048,
"preview": "Creative Commons Legal Code\n\nCC0 1.0 Universal\n\n CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE\n"
},
{
"path": "formats/fnt.md",
"chars": 3604,
"preview": "A file with the `.fnt` extension contains font data created by [playdate caps](https://play.date/caps/). It is a line or"
},
{
"path": "formats/luac.md",
"chars": 5617,
"preview": "Lua-based Playdate games use a tweaked version of Lua 5.4.3. You will only find compiled Lua bytecode in [`.pdz`](/forma"
},
{
"path": "formats/pda.md",
"chars": 2228,
"preview": "A file with the `.pda` extension represents audio data that has been compiled by `pdc`. This format uses little endian b"
},
{
"path": "formats/pdex.md",
"chars": 2958,
"preview": "The `pdex.bin` file represents information and executable code copied by `pdc` from a `pdex.elf` ELF file compiled for A"
},
{
"path": "formats/pdi.md",
"chars": 2899,
"preview": "A file with the `.pdi` extension represents a 1-bit bitmap image that has been compiled by `pdc`. This format uses littl"
},
{
"path": "formats/pds.md",
"chars": 1541,
"preview": "A file with the `.pds` extension represents a collection localization strings that have been compiled by `pdc`. This for"
},
{
"path": "formats/pdt.md",
"chars": 1897,
"preview": "A file with the `.pdt` extension represents a 1-bit bitmap image table containing multiple sub-images (like a spriteshee"
},
{
"path": "formats/pdv.md",
"chars": 2533,
"preview": "A file with the `.pdv` extension represents a 1-bit video that has been converted by `1bitvideo.app`. This format uses l"
},
{
"path": "formats/pdz.md",
"chars": 2561,
"preview": "A file with the `.pdz` extension represents a file container that has been compiled by `pdc`. They mostly contain compil"
},
{
"path": "formats/pft.md",
"chars": 3638,
"preview": "A file with the `.pft` extension represents a 1-bit bitmap font that has been compiled by `pdc`. This format uses little"
},
{
"path": "readme.md",
"chars": 3091,
"preview": "Unofficial Playdate reverse-engineering notes/tools - covers file formats, server API and USB serial commands\n\n> ⚠️ This"
},
{
"path": "server/api.md",
"chars": 7248,
"preview": "This is the API that is used by the Playdate console for things like fetching game updates, scoreboards, player details,"
},
{
"path": "tools/pdex2elf.py",
"chars": 12183,
"preview": "import argparse\nimport hashlib\nimport zlib\n\nif __name__ == '__main__':\n parser = argparse.ArgumentParser(\n pro"
},
{
"path": "tools/pdi2png.py",
"chars": 3790,
"preview": "#!/usr/bin/env python3\n# PDI to PNG converter\n# PDI docs: https://github.com/jaames/playdate-reverse-engineering/blob/ma"
},
{
"path": "tools/pdz.py",
"chars": 5866,
"preview": "# based on https://gist.github.com/zhuowei/666c7e6d21d842dbb8b723e96164d9c3\n# PDZ docs: https://github.com/jaames/playda"
},
{
"path": "tools/usbeval.py",
"chars": 4240,
"preview": "if (len(argv) < 2):\n print('usbeval.py')\n print('Evaluates a Lua script on a Playdate device, via USB')\n print('Requi"
},
{
"path": "usb/stream.md",
"chars": 4457,
"preview": "# The `stream` protocol\n\nWhen `stream enable` is sent over serial, the Playdate enters streaming mode, used by Playdate "
},
{
"path": "usb/usb.md",
"chars": 12399,
"preview": "When connected to USB and unlocked, the Playdate provides a serial interface over USB. Commands are sent as ascii and mu"
},
{
"path": "usb/usb_unlocked.md",
"chars": 6416,
"preview": "## USB commands in unlocked mode\n\nThis is the output of running `help` on a Playdate with firmware 1.10 after running `u"
}
]
About this extraction
This page contains the full source code of the jaames/playdate-reverse-engineering GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 22 files (94.0 KB), approximately 28.2k tokens, and a symbol index with 18 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.