feat(firmware): wire SD persistence + git publish into the editor

Land the v0.1 editor integration: the git_sync module (libgit2 on the SD
/sd/repo, dedicated 96KB git thread, lazy Wi-Fi, :sync push with
synced/up-to-date/failed snackbars), the boot splash (Spike 9) plus its bin
and justfile recipes, and a power-on→cursor boot-timing log. Also re-syncs
the roadmap/spikes/v0.1-product status and adds the SD hardware reference
photo.
This commit is contained in:
Julien Calixte
2026-07-11 15:30:43 +02:00
parent 98a9d1dffe
commit 8dc6ee362f
11 changed files with 710 additions and 65 deletions

View File

@@ -10,13 +10,22 @@ into are tracked in [`qfd.md`](qfd.md).
The editor **core** has been built 23 versions ahead of the device
**releases**, and is now **extracted into a host-testable `editor` crate** (plus
a `display` crate for the panel framebuffer) so `cargo test` exercises it off the
xtensa target. No release has shipped, but v0.1 is close: SD storage, save, and
**git publish are all wired into the app binary and hardware-verified
2026-07-11** (`:sync` commits on the SD `/sd/repo` and pushes to a test repo),
leaving the **boot splash as the last v0.1 gate** — and v0.2 navigation,
**v0.2.5 international input (hardware-verified 2026-07-11)**, and most of v0.6
Markdown already run. Version numbers are unchanged — they track shippable device
releases, not core progress.
xtensa target. **v0.1 shipped 2026-07-11** (late against the 2026-06-29
baseline): SD storage, save, and **git publish are all wired into the app binary
and hardware-verified** (`:sync` commits on the SD `/sd/repo` and pushes to a
test repo), and the **boot splash (Spike 9) is confirmed on the panel** — a
vector `typoena`-in-a-circle shown at startup while the SD mounts, then the
editor comes up. **Cold boot measured ~5.5 s** (power-on → cursor, instrumented
2026-07-11 — the 3.5 s eyeball undercounted; `esp_timer` sees only the ~4.1 s
app-side slice, missing the bootloader + ~0.74 s PSRAM memtest). **Over the ≤ 5 s
gate**; **fix implemented 2026-07-11** — the first editor render is now a
full-area partial (~630 ms) rather than a second full refresh (~1.9 s), expected
~4.2 s, pending a reflash to re-measure + check ghosting (PSRAM memtest kept on).
The 1-hour soak is attested from real use; boot time joins the
remaining post-ship acceptance checks (power-pull recovery, 1000-word no-drop,
and `Ctrl-G`'s not-yet-built pull-then-retry → v0.9). Beyond v0.1, v0.2 navigation, **v0.2.5 international input (hardware-verified
2026-07-11)**, and most of v0.6 Markdown already run. Version numbers are
unchanged — they track shippable device releases, not core progress.
Marks: `[x]` done in core · `[~]` partially done · `[ ]` not started. An
inline `(✓)` marks the done half of a split item.
@@ -34,8 +43,8 @@ title = "Typoena — macro plan"
name = "v0.1 it writes, it pushes"
start = 2026-06-01
original = 2026-06-29
status = "at-risk"
note = "Overdue — core editing, SD storage, and on-device git push all proven in spikes; SD mount + save now wired into main.rs. Remaining: boot splash and wiring git publish into main.rs."
delivered = 2026-07-11
learning = "Shipped 12 days late. The long pole was hardware bring-up risk, not the editor: SD on a shared SPI bus (resolved by moving it to its own SPI3, ADR-012) and on-device git (gix killed, pivoted to libgit2 as an esp-idf CMake component, ADR-004). Splash landed as a vector wordmark, not the planned 1-bit bitmap — the asset-embed/blit path is deferred to v1.0."
[[feature]]
name = "v0.2 navigation"
@@ -106,19 +115,29 @@ requires = ["v0.1 it writes, it pushes"]
---
## v0.1 — MVP: "it writes, it pushes" — [~]
## v0.1 — MVP: "it writes, it pushes" — [x]
The minimum thing that justifies the hardware existing. Full design:
[product](v0.1-mvp-product.md) · [technical](v0.1-mvp-technical.md).
**Status:** core editing + partial refresh run on device, and **SD mount + save
are now wired into `main.rs`** (Spike 3 resolved — a genuine ≤32 GB card mounts,
verified on its own SPI3 host per ADR-012). **Git publish is now wired too**
(`:sync` → commit + fast-forward push on the SD `/sd/repo`, hardware-verified
2026-07-11 against a test repo). Remaining v0.1 integration: the boot splash
(Spike 9).
**Status:** SHIPPED 2026-07-11 (late vs the 2026-06-29 baseline). Core editing +
partial refresh run on device; **SD mount + save are wired into `main.rs`**
(Spike 3 resolved — a genuine ≤32 GB card mounts, verified on its own SPI3 host
per ADR-012); **git publish is wired** (`:sync` → commit + fast-forward push on
the SD `/sd/repo`, hardware-verified against a test repo); and the **boot splash
(Spike 9) is confirmed on the panel** — [`Frame::splash`](../display/src/lib.rs)
shows a vector `typoena`-in-a-circle at startup while the SD mounts, then the
editor comes up. Cold boot measured ~5.5 s (power-on → cursor, 2026-07-11) — **over the ≤ 5 s
gate**: the 3.5 s eyeball undercounted, and `esp_timer` catches only the ~4.1 s
app-side slice (it starts after the bootloader + the ~0.74 s PSRAM memtest). Fix
implemented 2026-07-11: first editor render is now a full-area partial (~4.2 s
expected, pending a reflash to verify); PSRAM memtest kept on. The 1-hour soak
is attested from real use; boot time joins the remaining post-ship acceptance
checks (power-pull recovery, 1000-word no-drop, `Ctrl-G` pull-then-retry → v0.9) —
see [product → acceptance](v0.1-mvp-product.md#acceptance-criteria).
- [~] ESP32-S3 boots (✓); e-ink shows Typoena splash + boot log — splash pending Spike 9
- [x] ESP32-S3 boots (✓); e-ink shows Typoena splash (✓ Spike 9, confirmed on
panel 2026-07-11); boot status surfaces via the panel snackbar (no serial on device)
- [x] USB host enumerates the Nuphy, key events reach the editor (Spike 4)
- [x] One hard-coded file (`/sd/repo/notes.md`) opens on boot — **wired in
`main.rs`** (`boot_storage` mounts the SD and loads the note; a missing
@@ -166,7 +185,7 @@ verified on its own SPI3 host per ADR-012). **Git publish is now wired too**
rather than a timer — a timed auto-dismiss would cost a ~630 ms full-area
e-ink flash purely to erase text, which the panel deliberately avoids (cf.
the dropped pending-accent marker in v0.2.5).
- [~] Partial refresh on edits (✓ Spike 5); save now wired (full-area partial
- [x] Partial refresh on edits (✓ Spike 5); save wired (full-area partial
repaint on `:w`)
Out of scope: Vim, palette, multiple files, branches, conflict handling.

View File

@@ -64,6 +64,22 @@ risk early.
clean full refresh. Mostly a feature — kept as a spike only to prove the
asset path. (Feeds v0.1's "e-ink shows Typoena splash + boot log".)
**Built 2026-07-11 as a *vector* splash**, not a bitmap. The frame is
[`display::Frame::splash`](../display/src/lib.rs) — the `typoena` wordmark
centred inside a stroked `Circle`, drawn with `embedded-graphics`, one clean
full refresh. It is shared by two callers: the
[`splash`](../firmware/src/bin/splash.rs) bench binary (`just flash-splash`)
and **`main.rs`'s boot path**, which shows it right after EPD init — replacing
the old white-clear baseline — while the SD mounts and the note loads, then a
second full refresh brings up the editor. Nothing is embedded, so the
image-asset pipeline named above is deliberately **left unproven** — an
acceptable trade because Spike 2 already covered vector + font rendering.
**Proposition, deferred to end-of-project polish (v1.0):** replace the vector
mark with an embedded 1-bit raster logo, which is when the asset-embed/blit
path would finally be exercised. **Confirmed on the panel 2026-07-11** — the
splash renders cleanly at boot, then the editor comes up. This closed the last
v0.1 display gate; v0.1 shipped the same day.
10. **Spike 10 — Dark / light theme.** Invert the 1-bit framebuffer (white on
black) and refresh. The invert is trivial (XOR at blit); the real unknowns
are (a) ghosting and refresh time on a predominantly-black panel — full-black

View File

@@ -205,7 +205,25 @@ to an engineering function with a measured target in
[qfd.md §6](qfd.md#6-critical-performance-budget); that's the
place to check before declaring an item done.
> **Status 2026-07-11:** v0.1 was declared **delivered**, with acceptance
> criteria run as **post-ship hardening**. The 1-hour soak is attested from
> sustained real use (below). Two criteria are known **not** to pass yet, tracked
> as follow-ups rather than silent passes: cold boot measured **~5.5 s**
> (instrumented 2026-07-11, over the 5 s gate — fix identified below), and
> `Ctrl-G`'s pull-then-retry path is **not yet implemented** (deferred to v0.9).
- [ ] After a cold boot with valid pre-flashed config, cursor is ready in ≤ 5 s.
**MEASURED 2026-07-11: ~5.5 s power-on → cursor**, over the gate (the
`boot: cursor ready` log prefix, 5508 ms, from `esp_log_timestamp` ≈ since
power-on; the 4091 ms my first instrument printed was only the app-side
slice — `esp_timer` doesn't start until ~1.4 s in, after the 2nd-stage
bootloader + the ~0.74 s PSRAM memtest, so it excludes them). **Fix
implemented 2026-07-11** (pending a reflash to re-measure + confirm no
ghosting): the first editor render is now a full-area partial (~630 ms)
instead of a second full refresh (~1.9 s), which should bring boot to
~4.2 s. The two boot full refreshes (~3.9 s) were the structural cost; the
PSRAM memtest (~0.74 s, left on — a real HW sanity check on a hand-wired
board) is a further lever, and both feed the v1.0 ≤ 3 s target.
- [ ] Typing a 1000-word paragraph never drops a keystroke and never lags
more than 300 ms behind the keyboard.
- [ ] `Ctrl-S` durably writes the file (verified by power-cycling immediately
@@ -215,8 +233,11 @@ place to check before declaring an item done.
has moved on since the last publish).
- [ ] Pulling power during typing never corrupts the file; the previous saved
state is recoverable.
- [ ] One hour of continuous typing without crash, freeze, or memory
exhaustion.
- [x] One hour of continuous typing without crash, freeze, or memory
exhaustion. — attested by the author 2026-07-11 from sustained real-use
sessions (no crash, freeze, or memory exhaustion since the editor was wired
up). Author attestation, not a controlled instrumented run; a heap-watermark
log over a timed session would make it rigorous.
## Non-goals as success criteria