Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .claude/skills/openpocket-rescue/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
name: openpocket-rescue
description: Rescue a Donner Pocket X / M-VAVE BlackBox that restarts in a loop or no longer boots, from a Mac with no extra hardware (make rescue, tools/uboot_rescue.py, over the chip's USB UBOOT boot state). Use when the installer's rescue mode cannot finish, the pedal loops after a bad image, or a flash left the app area broken. Covers when to use it, the safety rules, who runs the sudo write, and how to read the result.
---

# Rescue a looping or bricked pedal

The pedal's mask ROM always enters the USB boot state ("UBOOT": USB `4C4A:8057`, shown as `WL80UBOOT1.00`) when the app is bad,
and it cannot be damaged. `tools/uboot_rescue.py` waits for that state, loads JieLi's RAM loader and rewrites the app area from
the official package. Full walkthrough and the record of the first real run: `docs/RESCUE.md`.

## When to use it

- The pedal restarts in a loop and the installer page's rescue mode (or `node tools/op.mjs flash`) cannot finish. Try those first:
they need no root.
- The pedal stays in the boot state (no loop, no sound) after an interrupted flash: the rescue finishes the job.
- Not for Bluetooth or firmware behaviour questions, and not on a pedal that works.

## Rules (read first)

1. **Read-only first.** `make rescue` writes nothing to the flash (the loader goes to RAM only) and saves a backup of the whole flash
to `~/OpenPocket-rescue`. Read its output before anything else.
2. **The captain runs the write.** `make rescue-write` and every `sudo` command need the captain's password and instruction. An
agent runs neither: prepare the exact command and give it with the `!` prefix.
3. The write range is only whole 4 KiB sectors in `[0x4000, end of the package)`, `[0x4000, 0xA3000)` for BlackBox. Never write
the head `[0, 0x4000)` or anything above the package. The tool refuses; do not work around it.
4. Never chip erase (`FB02`) and never burn a key (`FC12`). The tool cannot send them; do not add them.
5. Stop on any `REFUSED` (wrong device, chip key not `0x980F`, flash not 1 MiB, head differs from the package). Nothing was written;
report the message.
6. Keep the first backup. Never commit or publish a backup or a package: they hold the official firmware.
7. After a restore, do not reinstall the Bluetooth test image until the cause of the loop is known.

## How

```
brew install libusb # once
make doctor # rescue lines: libusb, official package, loader, root
make rescue # read-only: INQUIRY, loader to RAM, key, flash id, backup, head check, sectors that differ
make rescue-write # only on the captain's instruction, after the read-only run passed
```

Options go after `ARGS=` (`make rescue ARGS="--package FILE.fwsc"`; `--image flash.bin`, `--loader FILE`, `--backup-dir DIR`,
`--attempts N`). Plug the pedal straight into the Mac (no hub). Start the command first; it waits for the pedal and starts by
itself the moment it appears (the pedal is in UBOOT about 44 s of every 70 s while it loops). Needs the official BlackBox package
(default `~/Documents/BlackBox (2).fwsc`) and Python 3.

## Reading the result

| Output | Meaning |
| --- | --- |
| `INQUIRY: WL82 UBOOT1.00 1.00` | the right device |
| `loader running: chip key 0x980f, flash id ... (1024 KiB)` | the checks passed; record the flash id |
| `backup saved: ...dump.bin` | the rollback copy is complete on disk |
| `head [0, 0x4000) matches the image` | the package fits this pedal |
| `N of 160 sectors ... differ` | what a write would do; a rerun after a partial write shows fewer |
| `Read-only run finished, nothing was written` | exit 0, the read-only run is done |
| `Done: N sectors written and verified` | exit 0 after `--write`; power-cycle the pedal |
| `the pedal went away ... waiting for it again` | the window closed; it resumes by itself |
| exit 1 `ERROR` | setup: libusb, `sudo`, missing package; the message names the fix |
| exit 2 `REFUSED` | a safety check stopped it, nothing written |
| `LIBUSB_ERROR_ACCESS` | run with `sudo` (`make rescue` does); if it persists as root, report it |

Save the output of a real run (`make rescue 2>&1 | tee rescue.log`) and add its numbers to `docs/RESCUE.md`.

## Tests

`make rescue-check` runs the tool against a simulated UBOOT pedal (protocol, guards, resume, no hardware); it is part of
`make test`. Hardware behaviour is not covered by any test.
11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,18 @@
# make serve the installer and editor on http://localhost:8765/ (offline install)
# make render IN=guitar.wav [OUT=out.wav] [PRESET=1] a WAV through the engine
# make appimage [DUMP=fm1-1.bin] fm1-300.bin for the SD-card recovery
# make rescue [ARGS=..] restore a looping BlackBox from its UBOOT state, read-only (needs sudo and libusb: docs/RESCUE.md)
# make rescue-write [ARGS=..] the same, writing the official firmware back (run `make rescue` first)
# make rescue-check the UBOOT rescue tool (tools/uboot_rescue.py) against a simulated pedal; needs only Python 3
# `make NATIVE=1 build` skips Docker (toolchain installed with firmware/setup.sh).
.PHONY: doctor build test package clean shell image serve render appimage
doctor build test package clean shell image serve:
.PHONY: doctor build test package clean shell image serve render appimage rescue rescue-write rescue-check
doctor build test package clean shell image serve rescue-check:
@node tools/op.mjs $@
render:
@node tools/op.mjs render "$(IN)" "$(OUT)" "$(PRESET)"
appimage:
@node tools/op.mjs appimage "$(DUMP)"
rescue:
@node tools/op.mjs rescue $(ARGS)
rescue-write:
@node tools/op.mjs rescue --write $(ARGS)
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ setting was still the official firmware's (it survives a warm reset), so the ped
the DSP slower and crackle, so v33 leaves them as the boot loader does again.) Being tested on the BlackBox. For a pedal that
restarts in a loop, the page has a **rescue mode** (step 2): tick it, press Connect, plug the pedal in; the install
starts the moment the pedal answers and halts its DSP at once. A BlackBox can always go back to its official firmware from the page (step 3). It is the same JieLi update system as the M-VAVE FM-1, where this method is
used by Felucca, CLIPPER and SLOOP. Keep the SD-card recovery at hand (`flash/`: fm1buddy on an ESP32-S3,
`makeimage.bat`).
used by Felucca, CLIPPER and SLOOP. If the pedal still loops, restore it from a Mac with no extra hardware: `make rescue` (read-only check and backup), then
`make rescue-write` (`docs/RESCUE.md`; it needs `brew install libusb`, sudo and the official BlackBox file). The second route is the
SD-card recovery (`flash/`: fm1buddy on an ESP32-S3, `makeimage.bat`).

**Without internet**: double-click `INSTALL-OFFLINE.bat` (Windows; nothing to install: a small PowerShell server,
`tools/serve_offline.ps1`) or run `make serve` (macOS, Linux; `tools/serve.py`, needs Python 3). It serves the installer
Expand Down
2 changes: 2 additions & 0 deletions docs/BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Apple silicon and other ARM machines Docker emulates it (Rosetta or QEMU); you c
| `make serve` | the installer and editor on `http://localhost:8765/` (offline install; needs Python 3 on the host) |
| `make render IN=guitar.wav [OUT=..] [PRESET=1]` | a DI recording through the engine |
| `make appimage DUMP=fm1-1.bin` | `fm1-300.bin` for the SD-card recovery |
| `make rescue` / `make rescue-write` | restore a looping BlackBox from its UBOOT state from a Mac (read-only first; asks for sudo; `ARGS="--package FILE"` for options): `docs/RESCUE.md` |
| `make rescue-check` | the UBOOT rescue tool (`tools/uboot_rescue.py`, see `docs/RESCUE.md`) against a simulated pedal; needs only Python 3 |

`make NATIVE=1 build` skips Docker and uses a toolchain installed on the machine (`sh firmware/setup.sh` installs it in
`~/.jieli`; Linux x86-64 only). `tests/run_tests.sh` also runs directly on macOS and Linux with only a C compiler, Node and
Expand Down
104 changes: 104 additions & 0 deletions docs/RESCUE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Rescuing a crash-looping BlackBox from a Mac (no extra hardware)

Use this when the pedal restarts in a loop and the installer's rescue mode cannot finish. Between crashes the pedal sits
about 44 seconds in the JieLi mask-ROM USB boot state ("UBOOT": USB 4C4A:8057, shown by macOS as `WL80UBOOT1.00`). The
mask ROM is chip ROM, so it cannot be damaged. `tools/uboot_rescue.py` waits for that state, loads JieLi's RAM loader and
rewrites the app area of the flash from the official package, then restarts the pedal.

## One-time setup (macOS)

```
brew install libusb
```

That is all. The tool is one Python 3 file that calls libusb through `ctypes` (no `pip install`). It needs two files:

- The official package, `~/Documents/BlackBox (2).fwsc` (BlackBox_016) by default, or `--package FILE.fwsc`, or a raw flash
image with `--image flash.bin`. Every CRC of a package is checked before it is used.
- JieLi's `wl82loader.bin`, downloaded once into the git-ignored `build/cache/` from kagaimiq/jl-uboot-tool (commit
`c32b28e`, the same pin as `flash/installer-fm1buddy-pocketx.bat`) and checked against SHA-256
`d41da6126760c9d66660bcc0cac8d27d221806c5e369a8036921efe68dca5376` on every run. It is not in this repository. `make clean`
removes the cache; `--loader FILE` uses a copy you downloaded yourself.

## The commands

`make rescue` and `make rescue-write` (or `node tools/op.mjs rescue [--write]`, the entry point next to `flash`) run the tool with
`sudo` for you and pass options on (`make rescue ARGS="--package FILE.fwsc"`). `make doctor` shows whether libusb, the package and
the loader are in place. The `sudo python3 tools/uboot_rescue.py` lines below are the same thing written out.

macOS keeps its own mass-storage driver on the pedal in this state, and libusb needs root to take the interface from it.
Plug the pedal straight into the Mac (no hub), start the command, and it waits for the pedal and starts by itself the moment
it appears.

1. **Read-only first.** Nothing is written to the pedal's flash; the loader goes to RAM only.

```
make rescue # = sudo python3 tools/uboot_rescue.py
```

It checks that the pedal answers INQUIRY as `WL82 UBOOT1.00`, loads the loader, reads the chip key (must be `0x980F`) and
the flash id (must be a 1 MiB flash), saves a backup of the whole flash to `~/OpenPocket-rescue/blackbox-<time>.dump.bin`,
compares the pedal's head `[0, 0x4000)` with the image's, reports how many sectors differ, and sends run-app. Keep this
backup: it is the only copy of what the pedal held before the rescue.

2. **The rescue**, only after the read-only run has passed:

```
make rescue-write # = sudo python3 tools/uboot_rescue.py --write
```

Same steps, then for every 4 KiB sector in `[0x4000, 0xA3000)` that differs: erase, write, read back, compare. When it prints
`Done`, power-cycle the pedal. If the pedal drops out of UBOOT in the middle (it reboots into the crashing app and comes
back), the tool waits and starts again by itself; running it again later also resumes. Only the sectors that still differ are
written, so a rerun is short.

Options (after `ARGS=` with make): `--check` (prerequisites only), `--package`, `--image`, `--loader`, `--backup-dir DIR`, `--attempts N` (UBOOT windows to try, default 10).
Exit codes: 0 done, 1 setup problem (libusb, root, files), 2 refused by a safety check, 130 stopped by Ctrl-C.

## What the first real run showed

The first run on a real, looping BlackBox was done by the captain, who reported that the rescue worked ("the rescue worked!").
Only the files the tool left behind are recorded here; the captain's terminal output was not kept.

| Item | What the files show |
| --- | --- |
| Backups | Two, in `~/OpenPocket-rescue`: `blackbox-20261105-213039.dump.bin` and `blackbox-20261105-213135.dump.bin`, 1,048,576 bytes each (a 1 MiB flash), written 56 s apart. They are byte for byte identical (same SHA-256), so both hold the flash from before any write. |
| What the pedal held | Application `BlackBox_936`, the Bluetooth test image that caused the loop. Its app area passes the package's own CRC checks. |
| Head `[0, 0x4000)` | Identical to the head of the official BlackBox_016 `flash.bin`, so the head check passes. |
| Sectors to write | 159 of the 160 sectors in `[0x4000, 0xA3000)` differed from BlackBox_016 (first `0x4000`, last `0xA2000`). |
| Restored package | BlackBox_016 (`BlackBox (2).fwsc`, the tool's default image). |
| Chip key, flash id | Unknown: they are printed, not saved. The tool's checks (key `0x980F`, 1 MiB flash) passed, because it went on to write. |
| Length of the UBOOT window, sectors written, time taken | Unknown: not saved. A later run should keep its output (`make rescue 2>&1 \| tee rescue.log`). |

The two backups' times are consistent with a read-only run followed by the `--write` run, but no file records which was which.

## Safety rules

- The default is read-only. Writing needs `--write`.
- Writes are limited to whole 4 KiB sectors in `[0x4000, end of the image)`; for the BlackBox package that is
`[0x4000, 0xA3000)`. The end comes from the package and can never be above `0xFF000`. The head `[0, 0x4000)` (header,
second-stage boot loader, chip key) and everything above the image (Bluetooth trims, user data) are never touched.
- Only a fixed list of loader commands can be sent. Chip erase (`FB02`) and key burn (`FC12`) are not on it. In a read-only
run the erase and write commands are not on it either.
- It stops without writing when the device is not `WL82 UBOOT1.00`, the chip key is not `0x980F`, the flash is not 1 MiB, or
the pedal's head differs from the image's head (a package for another layout).
- The backup is complete on disk before the first erase.
- After a restore, do not reinstall the Bluetooth test image until the cause of the crash loop is understood: the loop came
from that image.

## If it does not work

| Symptom | Cause and next step |
| --- | --- |
| `libusb not found` | `brew install libusb` (or set `LIBUSB_PATH`). |
| `cannot take the interface from macOS (LIBUSB_ERROR_ACCESS)` | Run the command with `sudo`. |
| Waits forever | The pedal is not in UBOOT right now. It is there about 44 s out of every 70 s while it loops; check `ioreg -p IOUSB` for `WL80UBOOT1.00`, a data cable and a direct port. |
| `REFUSED` | A safety check stopped it; the message names which. Nothing was written. |
| `gave up after N tries` | The pedal kept leaving UBOOT before the run could finish. Rerun: it resumes. |
| Still nothing | The SD-card recovery in `flash/` (fm1buddy on an ESP32-S3) is the other route. |

## Credits

The USB boot protocol is documented by [kagaimiq/jl-uboot-tool](https://github.com/kagaimiq/jl-uboot-tool) (MIT) and
[konsumer/fm1buddy](https://github.com/konsumer/fm1buddy) (GPL-3.0); `tools/uboot_rescue.py` is a new implementation of the
same commands for macOS. Tests: `make rescue-check` runs it against a simulated pedal (`tests/uboot_sim.py`), with no hardware.
2 changes: 2 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ <h2><b>3</b><span data-t="s3"></span></h2>
<details>
<summary data-t="rescue"></summary>
<p class="note" data-t="rescuenote"></p>
<p class="note" data-t="rescuemac"></p>
<div class="row"><button class="alt" id="uboot" disabled data-t="uboot"></button></div>
</details>
<details>
Expand Down Expand Up @@ -144,6 +145,7 @@ <h2><b>3</b><span data-t="s3"></span></h2>
unknownok: "I understand: this file has not been tested with this page. Install it anyway.",
rescue: "Rescue and advanced options",
rescuenote: "If the pedal stays in update mode, reconnect it: the Resume button appears. The button below puts a pedal running OpenPocket into the chip's UBOOT mode, for a recovery from the SD card (fm1buddy).",
rescuemac: "A pedal that restarts in a loop and cannot be reached by this page can be restored from a Mac, with no extra hardware: download the official BlackBox file (M-VAVE), then run `make rescue` in the OpenPocket folder (read-only check), and `make rescue-write` to restore it. The steps are in docs/RESCUE.md; the SD-card route (fm1buddy, flash/) is the second choice.",
uboot: "Enter UBOOT mode", logt: "Log",
foot1: "OpenPocket is an independent project, not affiliated with Donner or M-VAVE. Install at your own risk. Source code: GPL-3.0.",
foot2: "Update protocol and loader from Felucca and felucca-bsp (Hügelton Instruments, GPL-3.0 / MPL-2.0). JieLi AC79 SDK files: Apache-2.0. Inter typeface: SIL OFL 1.1.",
Expand Down
3 changes: 3 additions & 0 deletions tests/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# - preset store: save / reload / power cut at every step of a save (firmware/px_store.h on a simulated flash)
# - packages: tools/pxpkg.py rebuilds the official PocketX_016 app area byte for byte (needs the official file)
# - installer: docs/pxota.js + docs/pxpkg.js against a simulated pedal running the real update code (ota.c, ldr_core.c)
# - rescue: tools/uboot_rescue.py (protocol, guards, resume) against a simulated UBOOT pedal, tests/uboot_sim.py
# - page: docs/index.html in headless Chromium (needs Playwright: npm i playwright && npx playwright install chromium)
set -e
cd "$(dirname "$0")/.."
Expand All @@ -25,6 +26,8 @@ echo "== installer"
node tests/test_installer.mjs "$1"
echo "== build entry point (tools/op.mjs)"
node tests/test_op.mjs
echo "== rescue tool (tools/uboot_rescue.py on a simulated UBOOT pedal)"
python3 tests/test_uboot_rescue.py $1
echo "== editor"
$CC -O1 -Wall -Wno-unused-function -o build/edsim tests/edsim.c engine/px_dsp.c firmware/px_math.c -lm
node tests/test_editor.mjs
Expand Down
Loading
Loading