From bddb272ae2a08d45f9d65b978d3186e836814689 Mon Sep 17 00:00:00 2001 From: isod89 Date: Tue, 3 Nov 2026 20:18:27 -0500 Subject: [PATCH 001/110] v30: set the power supplies at start as the stock firmware does (issue #1, BlackBox boot loop); installer rescue mode Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LFaxqW3CpFrLCvqoteTUv9 --- README.md | 15 +++++++++----- docs/AUDIO-SDK.md | 18 +++++++++++++++++ docs/index.html | 42 ++++++++++++++++++++++++++++++++++++---- docs/pxota.js | 15 ++++++++++++++ firmware/openpocket.c | 32 +++++++++++++++++++++++++++--- tests/test_installer.mjs | 24 +++++++++++++++++++++++ tests/test_page.mjs | 17 +++++++++++++++- 7 files changed, 150 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 1d377bf..eba1b40 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # OpenPocket -install here "for the Donner Pocket X. NOT for the M-VAVE BlackBox yet (boot loop, issue #1)" : https://isod89.github.io/openpocket/ +install here: https://isod89.github.io/openpocket/ (Donner Pocket X; M-VAVE BlackBox: v30 has the fix for issue #1, being tested) **Open-source guitar multi-effects firmware for the Donner Pocket X and the M-VAVE BlackBox.** Written from scratch for the pedal's own chip (JieLi AC791N, two cores), installable from the browser. @@ -40,10 +40,15 @@ USB, press **Connect**, then **Install**. The page also takes the pedal back to official `.fwsc` from the maker, it is checked in the browser. On the **Donner Pocket X** the page works: updating OpenPocket → a newer OpenPocket is tested (2026-11-02, your saved -presets are kept), and other Pocket X owners have installed it from the page. The **M-VAVE BlackBox is not supported -yet**: on a BlackBox, OpenPocket restarts in a loop a moment after starting ([issue #1](https://github.com/isod89/openpocket/issues/1)), -so the page does not install it there; a BlackBox that runs OpenPocket can be taken 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 +presets are kept), and other Pocket X owners have installed it from the page. On the **M-VAVE BlackBox**, OpenPocket up to v29 restarted in a loop after +the pedal was switched off and on ([issue #1](https://github.com/isod89/openpocket/issues/1)): it never set the power +supplies the way M-VAVE's own firmware does at start (core 1.26 → 1.38 V, VDC14 1.40 → 1.60 V, the DC-DC on). Right +after an install the pedal still had the levels the official firmware left (they survive a warm reset), so it worked; +after a power cycle it ran on the boot loader's lower levels and reset under DSP load. **v30 sets them at start**, +on both pedals, and shows them on the boot screen (`PWR 3>7 11>15 ...`). It is 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 the upgrade command stops its DSP, so it holds still long +enough. 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`). diff --git a/docs/AUDIO-SDK.md b/docs/AUDIO-SDK.md index 8c7fd5a..8917f43 100644 --- a/docs/AUDIO-SDK.md +++ b/docs/AUDIO-SDK.md @@ -169,3 +169,21 @@ Driver setup: `iis_open` IIS_PORTA, channel_in = BIT(1), channel_out = BIT(0), m (v25..v28) is converted at boot; the first save writes format 2 to the other copy. - Instructions per sample (PC, scalar): B3 ORGAN 2,329 (the heaviest new preset, as heavy as DOOM SLUDGE 2,311); others 709–1,795. Engine 339,660 bytes in POOL (344,064). + +## v30: the power supplies at start (issue #1, BlackBox boot loop) +- Report: OpenPocket on a BlackBox worked for an hour after the install, then, after the power switch, restarted every + ~4.3 s (USB up 2.9 s, down 1.3 s), on battery and on USB alike. +- BlackBox_020 and PocketX_016 drive the hardware identically (same pins, same register values, same `isd_config`); + both run the same power init at start (BB `f_0200698a`, PX `f_0200664a`): P3_ANA_CON0 LDO on → DC-DC on → LDO off + (1 ms apart), CON1 0x20, CON5 0x40 → [3:0] 4 → [5:4] 3 (0x74), CON6 12 → [2:0] 7, CON7 0xA5, CON8 0x96, + CON9 |= 0x10 (SYSVDD_VOL_HD_SEL) → [3:0] 15, CON0 |= 0x80, ANA_KEEP [5:0] 0, CON2 |= 0x40. +- OpenPocket did none of it (the BSP had `fm1_vdc14_raise`, `fm1_sysvdd_raise`, `fm1_power_stock`, never called). The + always-on P33 keeps these across a warm reset, so after an install (the official firmware ran last) the levels + were the stock ones; after a power cycle, the boot loader's (VDC14 3 = 1.40 V, SYSVDD 11 = 1.26 V, LDO), too low + for both cores at full load on some units. +- `fm1_cstart` now raises VDC14 to 7 and SYSVDD to 15 (a level per 100 us), sets the stock CON6 / CON9 bits, moves + the 1.3 V supply onto the DC-DC, CON5 0x74, before anything else runs. The boot screen shows `PWR a>b c>d DCDC ...` + (as found > as set). +- Boot OK now at 6 s (4.5 s of DSP), so a pedal that resets under load is caught by the guard and the safe mode. +- Installer: rescue mode (packages fetched beforehand; the install starts the moment a pedal answers; the upgrade + command halts the DSP and the second core at once). BlackBox install allowed again, with a note. diff --git a/docs/index.html b/docs/index.html index 80a33aa..c7b6ee5 100644 --- a/docs/index.html +++ b/docs/index.html @@ -83,7 +83,8 @@

1

2

…

- + +
@@ -146,7 +147,8 @@

3

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.", e_notfound: "Pedal not found. Check the cable (data), close other MIDI apps, then try again.", - bbwarn: "The M-VAVE BlackBox is not supported yet: with OpenPocket it restarts in a loop a moment after starting (issue #1 on GitHub). Installing on a BlackBox is disabled until this is fixed. If your BlackBox already runs OpenPocket, use step 3 to put its official firmware back.", + bbwarn: "M-VAVE BlackBox: v30 fixes issue #1 (OpenPocket restarted in a loop after the pedal was switched off and on), but it is still being tested on the BlackBox. Please report how it goes on GitHub. If your BlackBox restarts in a loop, use the rescue mode below.", + rescueon: "Rescue mode, for a pedal that restarts in a loop: tick this, press Connect, then plug the pedal in (or let it restart). The install starts by itself as soon as the pedal answers; it stops the pedal's sound processing at once, so the restarts stop. It installs the official file chosen in step 3 if there is one for this pedal, otherwise OpenPocket.", e_model: "This package is not for this pedal: ", e_lost: "The pedal was disconnected. Plug it back in and press Resume.", e_stopped: "The pedal stopped answering. ", e_noloader: "The update loader did not appear. Replug the cable, then Resume.", e_noreturn: "The pedal did not come back: switch it off and on.", e_mismatch: "Written, but the pedal reports: ", @@ -197,7 +199,7 @@

3

$("d-port").textContent = d.name; const bb = id.model === "BlackBox"; /* not supported yet: boot loop (issue #1) */ $("bbwarn").hidden = !bb; - $("install").disabled = busy || ota || !manifest || bb; + $("install").disabled = busy || ota || !manifest; $("resume").hidden = !ota; $("uboot").disabled = busy || !IS_OPENPOCKET(id); $("backoff").disabled = busy || ota || !official || official.model !== id.model || (!official.known && !$("offok").checked); @@ -208,7 +210,7 @@

3

if (!access) { access = await navigator.requestMIDIAccess({ sysex: true }); up = new Updater(access, log); - access.onstatechange = () => { if (!busy) scan(); }; + access.onstatechange = () => { if (busy || scanning) return; if (rescueArmed()) rescueTry(); else scan(); }; } await scan(); } catch (e) { setState(t("none"), "err"); log("MIDI: " + e.message); } @@ -224,6 +226,7 @@

3

else { setState(t("none"), "warn"); log("no pedal" + up.seen()); } showDev(d); } finally { scanning = false; } + if (rescueArmed() && lastDev && MODELS.includes(lastDev.id.model)) setTimeout(rescueTry, 0); // seen by a scan } function progress(kind, n) { @@ -267,6 +270,37 @@

3

} $("connect").onclick = connect; + +// rescue mode (issue #1): the packages are fetched beforehand, the install fires the moment a pedal answers +let pre = null; +let rescueTries = 0; // a few attempts, then the mode turns itself off +const rescueArmed = () => $("rescueOn").checked && pre !== null && up !== null && rescueTries < 10; +async function prefetch() { + pre = null; + const got = {}; + for (const m of MODELS) { try { got[m] = await packageFor(m); } catch (e) { log("rescue: " + errText(e)); } } + if ($("rescueOn").checked) { pre = got; log("rescue mode armed: waiting for the pedal"); setState(t("searching"), "warn"); } +} +function pick(model) { + if (official && official.model === model && (official.known || $("offok").checked)) return { image: official.image, product: official.product }; + return pre && pre[model]; +} +function rescueTry() { + if (busy || scanning || !rescueArmed()) return; + rescueTries++; + return run(async () => { + const r = await up.rescue(pick, (s, n) => { progress(s, n); if (s !== "verify" && s !== "write") log("rescue step " + s + (n !== undefined ? " " + n : "")); }); + if (!r) throw Object.assign(new Error("no pedal yet"), { code: "notfound", detail: up.seen() }); + $("rescueOn").checked = false; pre = null; + return r; + }); +} +$("rescueOn").onchange = async () => { + if (!$("rescueOn").checked) { pre = null; log("rescue mode off"); return; } + rescueTries = 0; + await prefetch(); + if (rescueArmed() && lastDev) rescueTry(); +}; $("install").onclick = () => run(async () => { const d = lastDev; const { product, image } = await packageFor(d.id.model); diff --git a/docs/pxota.js b/docs/pxota.js index bd374ca..c6758ff 100644 --- a/docs/pxota.js +++ b/docs/pxota.js @@ -200,6 +200,21 @@ export class Updater { const dev = await this.find((id) => MODELS.includes(id.model)); if (!dev) throw fail("notfound", "pedal not found", this.seen()); if (dev.id.model !== model) { dev.link.close(); throw fail("model", `the pedal is a ${dev.id.text}, the package is ${product}`, dev.id.text); } + return this.installOn(dev, image, product, step); + } + + // rescue (issue #1): a pedal that restarts every few seconds. The first pedal that answers gets the package of + // its model at once (pick(model) -> { image, product }, already fetched): the upgrade command stops its DSP. + async rescue(pick, onStep) { + const step = onStep || (() => {}); + const dev = await this.find((id) => MODELS.includes(id.model)); + if (!dev) return null; + const pkg = pick(dev.id.model); + if (!pkg) { dev.link.close(); throw fail("pkg", dev.id.model, dev.id.model); } + return this.installOn(dev, pkg.image, pkg.product, step); + } + + async installOn(dev, image, product, step) { step("start", dev.id.text); dev.link.send(UPGRADE); await sleep(this.t.afterUpgrade); diff --git a/firmware/openpocket.c b/firmware/openpocket.c index 7f4ab22..18ee66e 100644 --- a/firmware/openpocket.c +++ b/firmware/openpocket.c @@ -353,7 +353,7 @@ static void fm1_fault(const fm1_crash_t *c) #define PH_PU (*(volatile uint32_t *)0x501D0u) #define PH_DIR (*(volatile uint32_t *)0x501C8u) #define ENC_MASK 0x3u /* PC0=A, PC1=B */ -#define OP_VERSION_NUM "29" +#define OP_VERSION_NUM "30" #define OP_VERSION "v" OP_VERSION_NUM /* shown at boot: changes with every released build */ #define PROBE 0 /* 1 = GPIO diagnostic: shows the pins that change at the bottom */ #define ADC_PROBE 0 /* 1 = ADC diagnostic: shows the channels that move at the bottom (C2:512) */ @@ -526,9 +526,10 @@ static int dbn_press(dbn_t *d, int raw) #define OPD_MAGIC 0x4F504442u enum { ST_NONE, ST_INPUTS, ST_PRESET, ST_ENG_INIT, ST_ENG_SET, ST_I2S, ST_UI, ST_RUN, ST_OK, ST_N }; static const char *const ST_NAME[ST_N] = {"?", "INPUTS", "PRESET", "ENGINE INIT", "ENGINE SET", "I2S START", - "UI DRAW", "FIRST 2 S", "OK"}; + "UI DRAW", "FIRST 6 S", "OK"}; volatile struct { uint32_t magic, stage, blocks, load, peak, nans, c1, loops, crash_seen, safe; } op_diag __attribute__((section(".noinit"))); +static struct { uint8_t vdc0, vdc1, sys0, sys1, dcdc; } op_pwr; /* the supplies at boot (fm1_cstart) */ static void op_stage(uint32_t s, const char *txt) { op_diag.stage = s; @@ -572,6 +573,15 @@ static void app_main(void) lcd_rect(0,0,240,240,UI_BG); ft_box(0, 98, 240, "OPENPOCKET", &FT_MD, UI_ACC, UI_BG, 1); ft_box(0, 126, 240, OP_VERSION, &FT_SM, UI_DIM, UI_BG, 1); +#if !AUDIO_PROBE + { /* the supplies as found -> as set (fm1_cstart): "PWR 3>7 11>15 DCDC" */ + char l[32]; l[0] = 0; + b_str(l, "PWR "); b_num(l, op_pwr.vdc0); b_str(l, ">"); b_num(l, op_pwr.vdc1); + b_str(l, " "); b_num(l, op_pwr.sys0); b_str(l, ">"); b_num(l, op_pwr.sys1); + b_str(l, op_pwr.dcdc ? " DCDC OFF>ON" : " DCDC ON"); + ft_box(0, 150, 240, l, &FT_SM, UI_DIM, UI_BG, 1); + } +#endif for (int i = 0; i < 6; i++) { fm1_wdt_feed(); fm1_delay_ms(100); } /* --- report of the previous boot, if it did not finish (or crashed) --- */ int safe = 0; @@ -660,7 +670,7 @@ static void app_main(void) } } else if (now - t_tuner >= 100u) { t_tuner = now; tuner_draw(tuner_estimate()); } #endif - if (!boots_ok && now > 2000u) { boots_ok = 1; guard.pending = 0; guard.failed = 0; op_diag.stage = ST_OK; } + if (!boots_ok && now > 6000u) { /* 4.5 s of DSP first: a pedal that resets under load is caught */ boots_ok = 1; guard.pending = 0; guard.failed = 0; op_diag.stage = ST_OK; } #if !AUDIO_PROBE if (a_hold && !safe && now > 1500u) a_hold = 0; /* DSP on */ { @@ -778,8 +788,22 @@ static void app_main(void) void fm1_cstart(void) { uint32_t *s, *d; + uint32_t vdc0, sys0, dcdc; fm1_time_init(); fm1_wdt_arm(0x0D); + /* v30, issue #1: the supplies M-VAVE's firmware sets as it starts (BlackBox_020 and PocketX_016 alike, their + * power init: P3_ANA_CON6 [2:0] = 7, CON9 [3:0] = 15, the 1.3 V supply on the DC-DC, CON7 0xA5, CON8 0x96). + * The boot loader leaves VDC14 1.40 V and SYSVDD 1.26 V; the always-on P33 keeps the stock levels across a + * warm reset, so OpenPocket ran fine right after an install, then reset under DSP load after a power cycle. */ + vdc0 = fm1_vdc14(); + sys0 = fm1_sysvdd(); + fm1_vdc14_raise(7u); + fm1_p33_or(FM1_P3_ANA_CON6, 0x08u); /* as stock: CON6 = 0x0F */ + fm1_p33_or(FM1_P3_ANA_CON9, 0x10u); /* SYSVDD_VOL_HD_SEL: the core supply's strong drive, as stock */ + fm1_sysvdd_raise(15u); + dcdc = (uint32_t)fm1_power_stock(); + fm1_p33_write(FM1_P3_ANA_CON5, 0x74u); /* VDDIOM 4, VDDIOW 3, VDDIO drive 1: stock's final value */ + fm1_wdt_feed(); if (guard.magic != 0x48575431u || guard.pending > 1u || guard.failed > 1u) { guard.magic = 0x48575431u; guard.failed = 0; guard.pending = 0; } @@ -790,6 +814,8 @@ void fm1_cstart(void) for (d = _bss_start; d < _bss_end; d++) *d = 0; for (s = _data_load, d = _data_start; d < _data_end; s++, d++) *d = *s; for (s = _rt_load, d = _rt_start; d < _rt_end; s++, d++) *d = *s; + op_pwr.vdc0 = (uint8_t)vdc0; op_pwr.vdc1 = (uint8_t)fm1_vdc14(); + op_pwr.sys0 = (uint8_t)sys0; op_pwr.sys1 = (uint8_t)fm1_sysvdd(); op_pwr.dcdc = (uint8_t)dcdc; fm1_guard_enable(FM1_GUARD_STACK | FM1_GUARD_WRITE | FM1_GUARD_BUS | FM1_GUARD_PC); app_main(); for (;;) ; diff --git a/tests/test_installer.mjs b/tests/test_installer.mjs index dc3dd36..4ca5dc8 100644 --- a/tests/test_installer.mjs +++ b/tests/test_installer.mjs @@ -137,6 +137,30 @@ ok(productOf(px) === "PocketX_924" && productOf(bb) === "BlackBox_924", "package "damaged package: nothing in the app area erased"); } +// 6a. rescue mode (issue #1): the first pedal that answers gets the package of its model at once +{ + const dump = join(TMP, "rescue.bin"); + const sim = simAccess([join(TMP, "flash_old.bin"), "--dump", dump]); + await sim.ready; + const u = new Updater(sim.access, null, FAST); + const asked = []; + const pick = (m) => { asked.push(m); return m === "PocketX" ? { image: logicalImage(px), product: "PocketX_924" } : null; }; + let res = null, err = null; + try { res = await u.rescue(pick, () => {}); } catch (e) { err = e; } + await sim.end(); + ok(!err && res === "PocketX_924" && asked.join() === "PocketX", `rescue: the pedal answers, its model's package is written ${err ? "(" + err.code + ")" : ""}`); + ok(py(appOf, dump, join(ROOT, "tools")) === "PocketX_924 CRC-OK", "rescue: flash holds app PocketX_924, every CRC right"); +} +{ + const sim = simAccess([join(TMP, "flash_old.bin")]); + await sim.ready; + const u = new Updater(sim.access, null, FAST); + let err = null; + try { await u.rescue(() => null, () => {}); } catch (e) { err = e; } + await sim.end(); + ok(err && err.code === "pkg", "rescue: no package for the pedal's model -> refused, nothing sent"); +} + // 6. the way back, step 1 with the real official package (when given): checkOfficial, then OpenPocket's ota.c checks // and stages the official loader (its boot area must be valid) const off = process.argv[2]; diff --git a/tests/test_page.mjs b/tests/test_page.mjs index 06a1a81..f095fa9 100644 --- a/tests/test_page.mjs +++ b/tests/test_page.mjs @@ -97,8 +97,23 @@ await page.waitForTimeout(1500); ok(/OpenPocket v24/.test(await page.textContent("#d-fw")), "the page now sees OpenPocket v24"); await page.screenshot({ path: join(OUT, "page_3_done.png"), fullPage: true }); ok(simLog.some((l) => /step2 rc=0/.test(l)), "the simulated pedal wrote the new app"); + +// rescue mode: armed with no pedal; the install starts by itself when the pedal appears (issue #1) +sim.stdin.end(); sim = null; simLog.length = 0; buf = ""; +await page.reload(); +await page.waitForTimeout(300); +await page.check("#rescueOn"); +await page.waitForFunction(() => /rescue mode armed/.test(document.getElementById("log").textContent), null, { timeout: 15000 }); +await page.click("#connect"); +await page.waitForTimeout(300); +ok(!/PocketX_9/.test(await page.textContent("#msg")), "rescue: armed, nothing happens without a pedal"); +startSim(); +await page.waitForFunction(() => /PocketX_924/.test(document.getElementById("msg").textContent), null, { timeout: 240000 }); +ok(/PocketX_924/.test(await page.textContent("#msg")), "rescue: the pedal appears, the install runs by itself"); +ok(!(await page.isChecked("#rescueOn")), "rescue: the mode turns itself off once done"); +await page.screenshot({ path: join(OUT, "page_4_rescue.png"), fullPage: true }); await browser.close(); -sim.stdin.end(); +if (sim) sim.stdin.end(); server.close(); console.log(failed ? `${failed} FAILED` : "page test passed", "| screenshots:", OUT); process.exit(failed ? 1 : 0); From 129b6a4772659e28cd1b2660a27281bc27bb63f9 Mon Sep 17 00:00:00 2001 From: isod89 Date: Wed, 4 Nov 2026 10:20:29 -0500 Subject: [PATCH 002/110] v31: turn the long-press reset off at start, as the stock firmware does (issue #1); show the reset source Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LFaxqW3CpFrLCvqoteTUv9 --- README.md | 20 ++++++++++---------- docs/AUDIO-SDK.md | 13 +++++++++++++ docs/index.html | 2 +- firmware/openpocket.c | 31 ++++++++++++++++++++++++++++--- 4 files changed, 52 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index eba1b40..14cbe07 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # OpenPocket -install here: https://isod89.github.io/openpocket/ (Donner Pocket X; M-VAVE BlackBox: v30 has the fix for issue #1, being tested) +install here: https://isod89.github.io/openpocket/ (Donner Pocket X; M-VAVE BlackBox: v31 has the fix for issue #1, being tested) **Open-source guitar multi-effects firmware for the Donner Pocket X and the M-VAVE BlackBox.** Written from scratch for the pedal's own chip (JieLi AC791N, two cores), installable from the browser. @@ -40,15 +40,15 @@ USB, press **Connect**, then **Install**. The page also takes the pedal back to official `.fwsc` from the maker, it is checked in the browser. On the **Donner Pocket X** the page works: updating OpenPocket → a newer OpenPocket is tested (2026-11-02, your saved -presets are kept), and other Pocket X owners have installed it from the page. On the **M-VAVE BlackBox**, OpenPocket up to v29 restarted in a loop after -the pedal was switched off and on ([issue #1](https://github.com/isod89/openpocket/issues/1)): it never set the power -supplies the way M-VAVE's own firmware does at start (core 1.26 → 1.38 V, VDC14 1.40 → 1.60 V, the DC-DC on). Right -after an install the pedal still had the levels the official firmware left (they survive a warm reset), so it worked; -after a power cycle it ran on the boot loader's lower levels and reset under DSP load. **v30 sets them at start**, -on both pedals, and shows them on the boot screen (`PWR 3>7 11>15 ...`). It is 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 the upgrade command stops its DSP, so it holds still long -enough. 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 +presets are kept), and other Pocket X owners have installed it from the page. On the **M-VAVE BlackBox**, OpenPocket up to v30 restarted every ~4.3 s after +the pedal was switched off and on ([issue #1](https://github.com/isod89/openpocket/issues/1)). M-VAVE's firmware +turns the chip's **long-press reset** off as its very first step (P33 `PINR_CON = 0`); OpenPocket did not, and on the +BlackBox a pin sits at its active level, so the chip reset itself 4 s after each start. Right after an install the +setting was still the official firmware's (it survives a warm reset), so the pedal worked until the next power cycle. +**v31 turns it off at start.** It also sets the power supplies the way the official firmware does (v30), and the boot +screen shows `PWR ...` and `RST PINR `. 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`). diff --git a/docs/AUDIO-SDK.md b/docs/AUDIO-SDK.md index 8917f43..09e69a8 100644 --- a/docs/AUDIO-SDK.md +++ b/docs/AUDIO-SDK.md @@ -187,3 +187,16 @@ Driver setup: `iis_open` IIS_PORTA, channel_in = BIT(1), channel_out = BIT(0), m - Boot OK now at 6 s (4.5 s of DSP), so a pedal that resets under load is caught by the guard and the safe mode. - Installer: rescue mode (packages fetched beforehand; the install starts the moment a pedal answers; the upgrade command halts the DSP and the second core at once). BlackBox install allowed again, with a note. + +## v31: the long-press reset (issue #1, the real cause) +- v30 on the BlackBox: `PWR 3>7 11>15 DCDC OFF>ON` (supplies set), and still a reset: "LAST BOOT FAILED, STOPPED AT + FIRST 6 S", no crash, BLOCKS 976 (2.8 s of audio), LOAD 40 %, NAN 0. The .noinit diagnostics survived, so a chip + reset, not a power loss; no CPU exception. +- The period, ~4.3 s, matches the long-press reset (P3_PINR_CON, 0x94: the SDK's `struct long_press` offers 4 s or + 8 s, rising or falling edge, on one wake-up pin) + the boot. BlackBox_020's app entry (`f_0200698a`) writes + PINR_CON = 0 before anything else; OpenPocket never touched it. The always-on P33 keeps 0 across a warm reset (an + install), not across a power cycle: then the boot loader / power-on value applies, and on the BlackBox the pin + sits at its active level (the Pocket X's does not). +- `fm1_cstart` now reads P3_RST_SRC and PINR_CON, then writes PINR_CON = 0, first thing. The boot screen shows + `RST PINR `, and "LAST BOOT FAILED" shows "RESET BY: ..." (P3_RST_SRC bits: 0 power-on, + 1 VDDIO low, 2 watchdog, 3 VCM, 4 long press, 5 1.2 V, 6 soft). diff --git a/docs/index.html b/docs/index.html index c7b6ee5..2402e5d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -147,7 +147,7 @@

3

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.", e_notfound: "Pedal not found. Check the cable (data), close other MIDI apps, then try again.", - bbwarn: "M-VAVE BlackBox: v30 fixes issue #1 (OpenPocket restarted in a loop after the pedal was switched off and on), but it is still being tested on the BlackBox. Please report how it goes on GitHub. If your BlackBox restarts in a loop, use the rescue mode below.", + bbwarn: "M-VAVE BlackBox: v31 is the fix for issue #1 (OpenPocket restarted every 4 s after the pedal was switched off and on), still being tested on the BlackBox. Please report how it goes on GitHub. If your BlackBox restarts in a loop, use the rescue mode below.", rescueon: "Rescue mode, for a pedal that restarts in a loop: tick this, press Connect, then plug the pedal in (or let it restart). The install starts by itself as soon as the pedal answers; it stops the pedal's sound processing at once, so the restarts stop. It installs the official file chosen in step 3 if there is one for this pedal, otherwise OpenPocket.", e_model: "This package is not for this pedal: ", e_lost: "The pedal was disconnected. Plug it back in and press Resume.", e_stopped: "The pedal stopped answering. ", e_noloader: "The update loader did not appear. Replug the cable, then Resume.", diff --git a/firmware/openpocket.c b/firmware/openpocket.c index 18ee66e..8a11903 100644 --- a/firmware/openpocket.c +++ b/firmware/openpocket.c @@ -353,7 +353,7 @@ static void fm1_fault(const fm1_crash_t *c) #define PH_PU (*(volatile uint32_t *)0x501D0u) #define PH_DIR (*(volatile uint32_t *)0x501C8u) #define ENC_MASK 0x3u /* PC0=A, PC1=B */ -#define OP_VERSION_NUM "30" +#define OP_VERSION_NUM "31" #define OP_VERSION "v" OP_VERSION_NUM /* shown at boot: changes with every released build */ #define PROBE 0 /* 1 = GPIO diagnostic: shows the pins that change at the bottom */ #define ADC_PROBE 0 /* 1 = ADC diagnostic: shows the channels that move at the bottom (C2:512) */ @@ -529,7 +529,7 @@ static const char *const ST_NAME[ST_N] = {"?", "INPUTS", "PRESET", "ENGINE INIT" "UI DRAW", "FIRST 6 S", "OK"}; volatile struct { uint32_t magic, stage, blocks, load, peak, nans, c1, loops, crash_seen, safe; } op_diag __attribute__((section(".noinit"))); -static struct { uint8_t vdc0, vdc1, sys0, sys1, dcdc; } op_pwr; /* the supplies at boot (fm1_cstart) */ +static struct { uint8_t vdc0, vdc1, sys0, sys1, dcdc, rst, pinr; } op_pwr; /* supplies, reset source, pin reset (fm1_cstart) */ static void op_stage(uint32_t s, const char *txt) { op_diag.stage = s; @@ -551,6 +551,16 @@ static const int8_t QDEC[16] = {0,-1,1,0, 1,0,0,-1, -1,0,0,1, 0,1,-1,0}; #include "px_store.h" /* the user's presets in flash */ #endif +/* P3_RST_SRC as read at boot: why the chip last reset (fm1_sys.h) */ +static void rst_names(char *l, uint32_t r) +{ + static const char *const N[7] = {"POWER-ON", "VDDIO LOW", "WATCHDOG", "VCM", "LONG PRESS", "1.2V", "SOFT"}; + int any = 0; + for (int i = 0; i < 7; i++) + if (r & (1u << i)) { if (any) b_str(l, "+"); b_str(l, N[i]); any = 1; } + if (!any) b_str(l, "NONE"); +} + static void app_main(void) { uint32_t boots_ok = 0; @@ -580,6 +590,9 @@ static void app_main(void) b_str(l, " "); b_num(l, op_pwr.sys0); b_str(l, ">"); b_num(l, op_pwr.sys1); b_str(l, op_pwr.dcdc ? " DCDC OFF>ON" : " DCDC ON"); ft_box(0, 150, 240, l, &FT_SM, UI_DIM, UI_BG, 1); + char h[9]; l[0] = 0; + b_str(l, "RST "); rst_names(l, op_pwr.rst); b_str(l, " PINR "); hex8(h, op_pwr.pinr); b_str(l, h + 6); + ft_box(0, 170, 240, l, &FT_SM, UI_DIM, UI_BG, 1); } #endif for (int i = 0; i < 6; i++) { fm1_wdt_feed(); fm1_delay_ms(100); } @@ -599,6 +612,10 @@ static void app_main(void) ft_box(0, 6, 240, "LAST BOOT FAILED", &FT_MD, RED, UI_BG, 1); l[0] = 0; b_str(l, "STOPPED AT: "); b_str(l, ST_NAME[st]); ft_box(0, 40, 240, l, &FT_SM, UI_TXT, UI_BG, 1); + if (!crashed) { + l[0] = 0; b_str(l, "RESET BY: "); rst_names(l, op_pwr.rst); + ft_box(0, 62, 240, l, &FT_SM, UI_ACC, UI_BG, 1); + } if (crashed) { l[0] = 0; b_str(l, "CRASH PC "); hex8(h, fm1_crash.pc); b_str(l, h); ft_box(0, 62, 240, l, &FT_SM, UI_ACC, UI_BG, 1); @@ -788,8 +805,15 @@ static void app_main(void) void fm1_cstart(void) { uint32_t *s, *d; - uint32_t vdc0, sys0, dcdc; + uint32_t vdc0, sys0, dcdc, rst0, pinr0; fm1_time_init(); + /* v31, issue #1: the stock firmware's first act is P3_PINR_CON = 0, the long-press reset off (BlackBox_020 + * f_0200698a). Left on, a pin held at its active level resets the chip every 4 s (the SDK offers 4 or 8 s): + * Megal's BlackBox reset every 4.3 s after a power cycle, never after an install (the always-on P33 kept the + * stock firmware's 0 across the warm reset). Read first, so the boot screen can show it. */ + rst0 = fm1_p33_read(FM1_P3_RST_SRC); + pinr0 = fm1_p33_read(0x94u); + fm1_p33_write(0x94u, 0); fm1_wdt_arm(0x0D); /* v30, issue #1: the supplies M-VAVE's firmware sets as it starts (BlackBox_020 and PocketX_016 alike, their * power init: P3_ANA_CON6 [2:0] = 7, CON9 [3:0] = 15, the 1.3 V supply on the DC-DC, CON7 0xA5, CON8 0x96). @@ -816,6 +840,7 @@ void fm1_cstart(void) for (s = _rt_load, d = _rt_start; d < _rt_end; s++, d++) *d = *s; op_pwr.vdc0 = (uint8_t)vdc0; op_pwr.vdc1 = (uint8_t)fm1_vdc14(); op_pwr.sys0 = (uint8_t)sys0; op_pwr.sys1 = (uint8_t)fm1_sysvdd(); op_pwr.dcdc = (uint8_t)dcdc; + op_pwr.rst = (uint8_t)rst0; op_pwr.pinr = (uint8_t)pinr0; fm1_guard_enable(FM1_GUARD_STACK | FM1_GUARD_WRITE | FM1_GUARD_BUS | FM1_GUARD_PC); app_main(); for (;;) ; From c76e8c8fc2290213d4f45f066b104158c223eb31 Mon Sep 17 00:00:00 2001 From: isod89 Date: Wed, 4 Nov 2026 14:57:07 -0500 Subject: [PATCH 003/110] loader: never erase the preset store during an update; test that presets survive Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LFaxqW3CpFrLCvqoteTUv9 --- loader/ldr_core.c | 4 ++-- tests/test_installer.mjs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/loader/ldr_core.c b/loader/ldr_core.c index be12ffa..662530a 100644 --- a/loader/ldr_core.c +++ b/loader/ldr_core.c @@ -34,8 +34,8 @@ #define LDR_APP_HI 0xA3000u /* Pocket X: PRCT [0, 0xA3000) (PocketX_016) */ #define LDR_REC_LO 0xA3000u /* update records live above the app area ... */ #define LDR_REC_HI 0xFF000u /* ... and below key_mac (Pocket X); OpenPocket stages at 0xE0000 */ -#define LDR_DATA_LO 0xFF000u /* OpenPocket has no store the scan must skip: an empty range */ -#define LDR_DATA_HI 0xFF000u +#define LDR_DATA_LO 0xF0000u /* OpenPocket's preset store (px_store.h, copies A and B): */ +#define LDR_DATA_HI 0xF4000u /* user data, never erased even if a sector ends like a record */ #define LDR_NSEC ((LDR_APP_HI - LDR_APP_LO) >> 12) #define LDR_FL_MAX 0x100000u /* flash.bin: at most the 1 MiB part */ diff --git a/tests/test_installer.mjs b/tests/test_installer.mjs index 4ca5dc8..cd7840b 100644 --- a/tests/test_installer.mjs +++ b/tests/test_installer.mjs @@ -161,6 +161,36 @@ ok(productOf(px) === "PocketX_924" && productOf(bb) === "BlackBox_924", "package ok(err && err.code === "pkg", "rescue: no package for the pedal's model -> refused, nothing sent"); } +// 6b. the presets survive an update: the store [0xF0000, 0xF4000) is never erased, even sectors that end like an +// update record (the loader's record scan must skip it) +{ + const src = join(TMP, "flash_store.bin"), dump = join(TMP, "store_after.bin"); + py(` +import sys +f = bytearray(open(sys.argv[1], "rb").read()) +def crc(b, c=0): + for x in b: + c ^= x << 8 + for _ in range(8): c = ((c << 1) ^ 0x1021 if c & 0x8000 else c << 1) & 0xFFFF + return c +for s in range(0xF0000, 0xF4000, 0x1000): + f[s:s + 0x1000] = bytes((i * 7 + s // 0x1000) & 0xFF for i in range(0x1000)) + r = bytearray(f[s + 0xF00:s + 0xF00 + 80]); r[6:8] = (0x5441).to_bytes(2, "little") + r[0:2] = crc(r[2:80]).to_bytes(2, "little"); f[s + 0xF00:s + 0xF00 + 80] = r +open(sys.argv[2], "wb").write(f)`, join(TMP, "flash_old.bin"), src); + const sim = simAccess([src, "--dump", dump]); + await sim.ready; + const u = new Updater(sim.access, null, FAST); + let res = null, err = null; + try { res = await u.install(logicalImage(px), "PocketX_924", () => {}); } catch (e) { err = e; } + await sim.end(); + const same = py(` +import sys +a = open(sys.argv[1], "rb").read()[0xF0000:0xF4000]; b = open(sys.argv[2], "rb").read()[0xF0000:0xF4000] +print("same" if a == b else "changed")`, src, dump); + ok(!err && res === "PocketX_924" && same === "same", `presets: an update leaves the preset store untouched (${same})`); +} + // 6. the way back, step 1 with the real official package (when given): checkOfficial, then OpenPocket's ota.c checks // and stages the official loader (its boot area must be valid) const off = process.argv[2]; From 0d02e4359b75196b70f96ce5cc6f8ac1fea2f3c2 Mon Sep 17 00:00:00 2001 From: isod89 Date: Wed, 4 Nov 2026 15:23:17 -0500 Subject: [PATCH 004/110] v32: web preset editor (live editing, 52 presets, export/import) over USB-MIDI SysEx Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LFaxqW3CpFrLCvqoteTUv9 --- README.md | 11 + docs/AUDIO-SDK.md | 11 + docs/EDITOR-PROTOCOL.md | 54 ++++ docs/editor.html | 615 +++++++++++++++++++++++++++++++++++++ docs/index.html | 4 +- docs/pxedit.js | 246 +++++++++++++++ firmware/openpocket.c | 7 +- firmware/px_editor.h | 261 ++++++++++++++++ firmware/px_usb.h | 2 + firmware/ui.h | 14 +- firmware/ui_colors.h | 16 + tests/edsim.c | 177 +++++++++++ tests/run_tests.sh | 5 +- tests/test_editor.mjs | 143 +++++++++ tests/test_editor_page.mjs | 226 ++++++++++++++ tools/publish_github.ps1 | 3 +- 16 files changed, 1778 insertions(+), 17 deletions(-) create mode 100644 docs/EDITOR-PROTOCOL.md create mode 100644 docs/editor.html create mode 100644 docs/pxedit.js create mode 100644 firmware/px_editor.h create mode 100644 firmware/ui_colors.h create mode 100644 tests/edsim.c create mode 100644 tests/test_editor.mjs create mode 100644 tests/test_editor_page.mjs diff --git a/README.md b/README.md index 14cbe07..b6a6f41 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,14 @@ starts the moment the pedal answers and halts its DSP at once. A BlackBox can al used by Felucca, CLIPPER and SLOOP. Keep the SD-card recovery at hand (`flash/`: fm1buddy on an ESP32-S3, `makeimage.bat`). +## Preset editor + +From OpenPocket **v32**, the **Preset editor** page (`editor.html`, linked from the installer) edits the pedal live over +USB, in Chrome or Edge: the 52 presets (load, save, save as, move up / down, back to factory), the current sound (every +block, type and setting, on / off, the block order by dragging the tiles, the name), and preset files (export one or +all 52 as JSON, import one to try it or many back into their slots). What you change on the page plays at once, and +what you change on the pedal shows on the page. The protocol: [`docs/EDITOR-PROTOCOL.md`](docs/EDITOR-PROTOCOL.md). + ## Build Windows: install WSL and the JieLi toolchain once (`wsl bash firmware/setup.sh`), then run **`build.bat`**. It makes: @@ -76,6 +84,9 @@ the packages, and the installer on GitHub Pages (with your own `git` / `gh` and `ldr_core.c`) on a simulated flash: update, install over the official firmware, wrong model refused, damaged package refused before anything is erased, way back to the official firmware (its loader staged and checked); - the page itself in headless Chromium (Playwright), clicking Connect and Install. +- the preset editor against a simulated pedal that runs the pedal's real editor code (`px_editor.h`) and preset store + on a simulated flash: every command, pushes both ways, a power cycle; then the editor page in Chromium (sliders, + drag and drop, save, move, export / import). ## Layout diff --git a/docs/AUDIO-SDK.md b/docs/AUDIO-SDK.md index 09e69a8..792934f 100644 --- a/docs/AUDIO-SDK.md +++ b/docs/AUDIO-SDK.md @@ -200,3 +200,14 @@ Driver setup: `iis_open` IIS_PORTA, channel_in = BIT(1), channel_out = BIT(0), m - `fm1_cstart` now reads P3_RST_SRC and PINR_CON, then writes PINR_CON = 0, first thing. The boot screen shows `RST PINR `, and "LAST BOOT FAILED" shows "RESET BY: ..." (P3_RST_SRC bits: 0 power-on, 1 VDDIO low, 2 watchdog, 3 VCM, 4 long press, 5 1.2 V, 6 soft). + +## v32: the web editor +- `firmware/px_editor.h`: SysEx `F0 7D 4F 50 cmd ... F7` (docs/EDITOR-PROTOCOL.md), frames from usb.c's `sx_frame` + (`ota_frame_get`), handled in the main loop before `ota_service` (which takes any frame), replies through + `ota_wire_send`, as Felucca's editor.c. It edits `PRESET` directly: `audio_poll` applies, `ui_render` redraws. +- Pushes: while watched, a hash of PRESET + slot + focused block + `st_gen`; a change made by the editor itself updates + the hash, so only the pedal's own changes are pushed (40 ms apart at most). The watch ends 5 s after the last request + (the page pings every second; PING also returns the DSP load). +- Slots: `SLOT_PUT` changes `st_slot` in RAM; `COMMIT` (or bit 1) writes the whole store once (`st_commit`): an import + of 52 presets is one flash write. `BLK_COL` moved to `ui_colors.h` for `CAT_BLOCK`. +- `tests/edsim.c` compiles px_editor.h + px_store.h + the engine catalogue on the PC (stdin / stdout like devsim). diff --git a/docs/EDITOR-PROTOCOL.md b/docs/EDITOR-PROTOCOL.md new file mode 100644 index 0000000..bbe15c2 --- /dev/null +++ b/docs/EDITOR-PROTOCOL.md @@ -0,0 +1,54 @@ +# OpenPocket editor protocol (SysEx over USB-MIDI) + +The web editor (`docs/editor.html`, `docs/pxedit.js`) talks to the pedal with this protocol. The firmware side is +`firmware/px_editor.h`; `tests/edsim.c` runs the same code on a PC for the tests. Protocol version 1 (OpenPocket v32). + +## Framing + +A request is `F0 7D 4F 50 F7` (`7D` = the non-commercial SysEx ID, `4F 50` = "OP"). Every request +gets exactly one reply with the same header and the same ``; a request the pedal does not understand gets no +reply. Every data byte is 7 bit. Send the next request only after the reply to the previous one (the pedal keeps +one incoming frame at a time). While the editor watches (`WATCH`), the pedal also sends `STATE` frames by itself. + +| Item | Encoding | +| --- | --- | +| byte | one 7-bit byte | +| string | ASCII 32..126, ended by a 0 byte | +| color | an RGB565 color in 3 bytes: bits 0-6, 7-13, 14-15 | +| preset `P` | 110 bytes, the firmware's `px_preset_t` as is (every byte is below 128): `name` 14 (ASCII, 0-padded, at most 13 characters), `on` 12 (0/1), `type` 12, `p` 12 x 5 (0..100), `order` 12 (a permutation of 0..11) | + +The 12 blocks, by index: 0 GATE, 1 COMP, 2 PITCH, 3 SFX, 4 DRIVE, 5 AMP, 6 CAB, 7 EQ, 8 MOD, 9 DELAY, 10 REVERB, +11 SYN (`CAT_BLOCK` gives their labels and names). `order` is the processing and display order. + +## Commands + +| cmd | Request args | Reply args | +| --- | --- | --- | +| 1 INFO | — | protocol (1), firmware string ("v32"), identity string ("PocketX_932"), blocks (12), parameters per block (5), slots (52), name length (13) | +| 2 CAT_BLOCK | block | block, label string ("AMP"), name string ("AMPLIFIER"), types, color | +| 3 CAT_TYPE | block, type | block, type, name string, parameters n, text mask (bit k: parameter k has its own value texts, see `TEXTS`), then n parameter-name strings | +| 4 TEXTS | block, type, k | block, type, k, then 101 strings: the text the pedal shows for values 0..100 ("" = the number itself) | +| 5 STATE | — | slot (0..51), flags (bit 0: the sound differs from the slot's, bit 1: the slot holds a saved preset), focused block, `P` (the current sound) | +| 6 SET | block, what, k, v | block, what, k, v (after clamping). what 0: parameter k = v (0..100); 1: on = v (0/1); 2: type = v | +| 7 ORDER | 12 bytes | rc (0 ok, 1 not a permutation) | +| 8 NAME | string | the name as stored (13 characters at most, other characters become "?") | +| 9 PUT | `P` | rc (0 ok, 1 invalid). Replaces the current sound (not saved) | +| 10 SELECT | slot | slot, rc. Loads the slot as the current sound (unsaved changes are dropped, as on the pedal) | +| 11 FOCUS | block | block. The pedal shows that block's page | +| 12 LIST | start, count (1..26) | start, count, then per slot: saved (0/1), name string | +| 13 SLOT_GET | slot | slot, saved, `P` (the factory preset when not saved) | +| 14 SLOT_PUT | slot, flags, [`P`] | slot, rc (0 ok, 1 invalid, 2 flash). flags bit 0: `P` follows and is stored as a saved preset (else the slot goes back to its factory preset); bit 1: write the flash now (else only in memory until `COMMIT`) | +| 15 COMMIT | — | rc (0 ok, 2 flash). Writes every slot to the flash (one write, about 0.3 s) | +| 16 SAVE | slot | slot, rc. Saves the current sound into the slot, writes the flash, the slot becomes the current one (as the pedal's SAVE) | +| 17 WATCH | on (0/1) | on. While on, the pedal pushes `STATE` (cmd 5) when the sound, the slot or the focused block changes on the pedal; it stops by itself 5 s after the last request | +| 18 PING | — | DSP load % (0..127), flags (bit 0: bypass, bit 1: second core) | + +Writing the flash mutes the sound for a moment, as saving on the pedal does. A whole import sends `SLOT_PUT` without +bit 1 for every slot, then one `COMMIT`. + +## Preset files + +The editor saves presets as JSON: `{"format": "openpocket-presets", "version": 1, "presets": [...]}`, each preset +`{"slot": 1..52, "name", "blocks": [{"block": "AMP", "on", "type": "type name", "params": [5 numbers]}...], +"order": ["GATE", ...]}`. Blocks and types are written by name, so a file stays readable and survives a firmware +that adds types; a single preset is the same object with one preset in `presets`. diff --git a/docs/editor.html b/docs/editor.html new file mode 100644 index 0000000..d56bb47 --- /dev/null +++ b/docs/editor.html @@ -0,0 +1,615 @@ + + + + + + +OpenPocket Editor + + + + + +
+
+
OPENPOCKETeditor
+
not connected
+
+ Installer + +
+ +
+ + +
+ + +
+
+
+

Connect your pedal

+

Plug the Pocket X or BlackBox in with USB, then press Connect and allow MIDI devices. The pedal needs + OpenPocket v32 or newer: older versions can be updated from the installer.

+

Everything you change here plays at once on the pedal, and what you change on the pedal shows here.

+
+ +
+ +
+
+
+ + + + diff --git a/docs/index.html b/docs/index.html index 2402e5d..fdc946d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -59,6 +59,7 @@

OpenPocket

+