diff --git a/README.md b/README.md index 08c5c6c..ea68249 100644 --- a/README.md +++ b/README.md @@ -28,14 +28,14 @@ push) is offered. ## Hardware -| Part | Choice | Why | -|---|---|---| -| MCU | **ESP32-S3-N16R8** (16 MB flash, 8 MB octal PSRAM) | USB OTG host (for the keyboard), Wi-Fi, BLE, dual core @ 240 MHz, plenty of PSRAM for git pack data and screen buffer. Best-supported Rust target in the ESP family. | -| Display | **GDEY0579T93 + DESPI-c579 breakout** (5.79", 792×272, 1-bit) | Good Display panel matched with its own FPC breakout. Strip aspect (~2.9:1) — Freewrite-coded: ~12 lines of edit area, ~95 cols. Tiny framebuffer (~27 KB) leaves PSRAM headroom. The DESPI-c579 is a passive level-shifter / FPC-to-header board, not an active controller — driven over plain SPI like any other epd. | -| Keyboard | **Nuphy Air60/Halo65 wired USB-C** | ESP32-S3 acts as USB host via TinyUSB. BLE-HID is a fallback but contends with Wi-Fi for radio time during push. | -| Storage | microSD over SPI | Holds both the git working copy (`/sd/repo/`) **and** the local-only scratch space (`/sd/local/`). Internal flash is for firmware + config only. | -| Power | **USB-C wall power for MVP**, 18650 + IP5306 in Phase 3 | Measure power profile on real hardware before sizing the battery. E-ink + sleep should give multi-day battery life but battery introduces charging, safety, and BMS complexity we don't need on day one. | -| Enclosure | 3D-printed, hinged lid | Phase 4 concern. | +| Part | Choice | Why | +| --------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MCU | **ESP32-S3-N16R8** (16 MB flash, 8 MB octal PSRAM) | USB OTG host (for the keyboard), Wi-Fi, BLE, dual core @ 240 MHz, plenty of PSRAM for git pack data and screen buffer. Best-supported Rust target in the ESP family. | +| Display | **GDEY0579T93 + DESPI-c579 breakout** (5.79", 792×272, 1-bit) | Good Display panel matched with its own FPC breakout. Strip aspect (~2.9:1) — Freewrite-coded: ~12 lines of edit area, ~95 cols. Tiny framebuffer (~27 KB) leaves PSRAM headroom. The DESPI-c579 is a passive level-shifter / FPC-to-header board, not an active controller — driven over plain SPI like any other epd. | +| Keyboard | **Nuphy Air60/Halo65 wired USB-C** | ESP32-S3 acts as USB host via TinyUSB. BLE-HID is a fallback but contends with Wi-Fi for radio time during push. | +| Storage | microSD over SPI | Holds both the git working copy (`/sd/repo/`) **and** the local-only scratch space (`/sd/local/`). Internal flash is for firmware + config only. | +| Power | **USB-C wall power for MVP**, 18650 + IP5306 in Phase 3 | Measure power profile on real hardware before sizing the battery. E-ink + sleep should give multi-day battery life but battery introduces charging, safety, and BMS complexity we don't need on day one. | +| Enclosure | 3D-printed, hinged lid | Phase 4 concern. | **Why the 5.79" strip aspect:** less screen than a 7.5" page-shaped panel, but the long-narrow shape biases toward "current line + recent context" — @@ -55,25 +55,25 @@ concurrency, storage, power, and keyboard transport. How each decision is weighted against the user-facing requirements — and the critical performance budget that falls out — lives in [`docs/qfd.md`](docs/qfd.md). -| Layer | Crate / Component | Notes | -|---|---|---| -| HAL / runtime | `esp-idf-svc`, `esp-idf-hal` | std build, gives us heap, threads, VFS, mbedtls, Wi-Fi stack. | -| Display | `embedded-graphics` + `epd-waveshare` (or custom driver) | Pixel framebuffer with partial-refresh regions. We track dirty rects ourselves. The GDEY0579T93 uses an SSD1683-class controller; if it's not already in `epd-waveshare`, we write a small driver against `embedded-hal` SPI — ~300 LoC, low risk. | -| Editor core | Custom, in-tree | Rope buffer (`ropey`), mode state machine, Vim keymap table. | -| TUI-style layout | Custom thin layer (~500 LoC) | API inspired by Ratatui (`Layout`, `Block`, `Paragraph`) but renders directly to `embedded-graphics`. See below. | -| USB host | `esp-idf` TinyUSB bindings | Boot-protocol HID is enough for the keyboard. | -| Git | `gitoxide` (`gix`) | Pure-Rust, modular. We only need add / commit / push (smart HTTP). No libgit2, no mbedtls glue beyond what `esp-idf` already gives us. | -| TLS | `mbedtls` via `esp-idf` | Used for GitHub HTTPS. ~120 KB heap during handshake — fits in PSRAM. | -| Auth | GitHub Personal Access Token in encrypted NVS | SSH on embedded is painful; HTTPS+PAT is the pragmatic path. | -| Filesystem | FAT on SD (`esp_vfs_fat`) | Working copy lives here. Internal LittleFS holds config. | +| Layer | Crate / Component | Notes | +| ---------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| HAL / runtime | `esp-idf-svc`, `esp-idf-hal` | std build, gives us heap, threads, VFS, mbedtls, Wi-Fi stack. | +| Display | `embedded-graphics` + `epd-waveshare` (or custom driver) | Pixel framebuffer with partial-refresh regions. We track dirty rects ourselves. The GDEY0579T93 uses an SSD1683-class controller; if it's not already in `epd-waveshare`, we write a small driver against `embedded-hal` SPI — ~300 LoC, low risk. | +| Editor core | Custom, in-tree | Rope buffer (`ropey`), mode state machine, Vim keymap table. | +| TUI-style layout | Custom thin layer (~500 LoC) | API inspired by Ratatui (`Layout`, `Block`, `Paragraph`) but renders directly to `embedded-graphics`. See below. | +| USB host | `esp-idf` TinyUSB bindings | Boot-protocol HID is enough for the keyboard. | +| Git | `gitoxide` (`gix`) | Pure-Rust, modular. We only need add / commit / push (smart HTTP). No libgit2, no mbedtls glue beyond what `esp-idf` already gives us. | +| TLS | `mbedtls` via `esp-idf` | Used for GitHub HTTPS. ~120 KB heap during handshake — fits in PSRAM. | +| Auth | GitHub Personal Access Token in encrypted NVS | SSH on embedded is painful; HTTPS+PAT is the pragmatic path. | +| Filesystem | FAT on SD (`esp_vfs_fat`) | Working copy lives here. Internal LittleFS holds config. | ### Why not Ratatui Ratatui assumes a **character-grid terminal** with an ANSI backend. E-ink is a **pixel framebuffer with partial-refresh windows**. The right primitive for e-ink is dirty-rectangle tracking aligned to the panel's refresh regions — -Ratatui's per-cell diff model fights this. We can borrow its widget *API -shape* (it's a good one) without dragging in the terminal abstraction. Net +Ratatui's per-cell diff model fights this. We can borrow its widget _API +shape_ (it's a good one) without dragging in the terminal abstraction. Net saving: probably 200 KB of binary and a lot of pretending the screen is a VT100. @@ -135,19 +135,19 @@ gantt v1.0 polish :v10, after v09, 4w ``` -| Version | Theme | One-liner | -|---|---|---| -| [v0.1](docs/roadmap.md#v01--mvp-it-writes-it-pushes--) | MVP | Boots, edits one file, `Ctrl-G` pushes. | -| [v0.2](docs/roadmap.md#v02--vim-navigation--) | Vim nav | Normal/Insert, motions, line numbers. | -| [v0.3](docs/roadmap.md#v03--vim-editing--) | Vim edit | `dd yy p`, undo/redo, counts. | -| [v0.4](docs/roadmap.md#v04--visual-mode--ex-commands--) | Visual + ex | `v V`, `:w :q :e`, status line. | -| [v0.5](docs/roadmap.md#v05--file-palette--multi-file--) | Files | `Ctrl-P` over `/repo` + `/local`, buffers. | -| [v0.6](docs/roadmap.md#v06--markdown-affordances--) | Markdown | Headings, list continuation, soft-wrap. | -| [v0.7](docs/roadmap.md#v07--search--better-git--) | Search + git | `/`, `:Gpull`, `:Gbranch`, commit msg. | -| [v0.8](docs/roadmap.md#v08--power-battery--sleep--) | Power | 18650 + sleep + lid switch. | -| [v0.9](docs/roadmap.md#v09--robustness--) | Robustness | Crash-safe writes, reconnect, settings. | -| [v1.0](docs/roadmap.md#v10--polish--) | Polish | Boot ≤ 3 s, fonts, enclosure, guide. | -| [v1.x](docs/roadmap.md#v1x--stretch--nice-to-have) | Stretch | 10.3" panel, spell-check, themes, BLE. | +| Version | Theme | One-liner | +| ------------------------------------------------------- | ------------ | ------------------------------------------ | +| [v0.1](docs/roadmap.md#v01--mvp-it-writes-it-pushes--) | MVP | Boots, edits one file, `Ctrl-G` pushes. | +| [v0.2](docs/roadmap.md#v02--vim-navigation--) | Vim nav | Normal/Insert, motions, line numbers. | +| [v0.3](docs/roadmap.md#v03--vim-editing--) | Vim edit | `dd yy p`, undo/redo, counts. | +| [v0.4](docs/roadmap.md#v04--visual-mode--ex-commands--) | Visual + ex | `v V`, `:w :q :e`, status line. | +| [v0.5](docs/roadmap.md#v05--file-palette--multi-file--) | Files | `Ctrl-P` over `/repo` + `/local`, buffers. | +| [v0.6](docs/roadmap.md#v06--markdown-affordances--) | Markdown | Headings, list continuation, soft-wrap. | +| [v0.7](docs/roadmap.md#v07--search--better-git--) | Search + git | `/`, `:Gpull`, `:Gbranch`, commit msg. | +| [v0.8](docs/roadmap.md#v08--power-battery--sleep--) | Power | 18650 + sleep + lid switch. | +| [v0.9](docs/roadmap.md#v09--robustness--) | Robustness | Crash-safe writes, reconnect, settings. | +| [v1.0](docs/roadmap.md#v10--polish--) | Polish | Boot ≤ 3 s, fonts, enclosure, guide. | +| [v1.x](docs/roadmap.md#v1x--stretch--nice-to-have) | Stretch | 10.3" panel, spell-check, themes, BLE. | --- diff --git a/docs/adr.md b/docs/adr.md index 3635f35..a866aca 100644 --- a/docs/adr.md +++ b/docs/adr.md @@ -23,6 +23,7 @@ components, with the tradeoffs from this file ranked by user-facing weight. **Scope:** Whole project. ### Context + The firmware needs: USB host, Wi-Fi + TLS, SPI peripherals, a SD filesystem, and a working git implementation that can push over HTTPS. All on an ESP32-S3 with 8 MB PSRAM. We also want the code to stay refactorable as features pile @@ -30,21 +31,23 @@ up across nine downstream releases. ### Options considered -| Option | Pros | Cons | -|---|---|---| -| **C / C++ on ESP-IDF** | Reference platform, every peripheral has a driver, fastest path to first pixel. | Refactoring at scale is painful; memory safety is on you. | -| **Rust on `esp-idf-rs` (std)** | First-class Espressif-sponsored Rust support; `std` gives heap / threads / VFS / mbedtls; can use the broader Rust ecosystem (`gitoxide`, `ropey`, `embedded-graphics`). | Larger binary than `no_std`; longer build times; some `unsafe` at FFI seams. | -| **Rust on `esp-hal` (no_std)** | Smallest binary, most "pure" embedded experience. | No `std` = no off-the-shelf git, no easy TLS, would re-implement a lot of plumbing. | -| **Gleam + Shore on AtomVM** | Beautiful language, the user's stated preference. | BEAM on ESP32 is memory-hungry; no bindings for USB host, e-ink, SD, TLS, git in that ecosystem. Two research projects stacked. | -| **MicroPython / CircuitPython** | Fastest to prototype. | Too slow for responsive editing at the latencies e-ink already imposes; GC pauses would surface as dropped keys. | -| **TinyGo** | Modern, ergonomic. | ESP32-S3 support is thinner than Rust's; smaller ecosystem of embedded crates equivalents. | +| Option | Pros | Cons | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | +| **C / C++ on ESP-IDF** | Reference platform, every peripheral has a driver, fastest path to first pixel. | Refactoring at scale is painful; memory safety is on you. | +| **Rust on `esp-idf-rs` (std)** | First-class Espressif-sponsored Rust support; `std` gives heap / threads / VFS / mbedtls; can use the broader Rust ecosystem (`gitoxide`, `ropey`, `embedded-graphics`). | Larger binary than `no_std`; longer build times; some `unsafe` at FFI seams. | +| **Rust on `esp-hal` (no_std)** | Smallest binary, most "pure" embedded experience. | No `std` = no off-the-shelf git, no easy TLS, would re-implement a lot of plumbing. | +| **Gleam + Shore on AtomVM** | Beautiful language, the user's stated preference. | BEAM on ESP32 is memory-hungry; no bindings for USB host, e-ink, SD, TLS, git in that ecosystem. Two research projects stacked. | +| **MicroPython / CircuitPython** | Fastest to prototype. | Too slow for responsive editing at the latencies e-ink already imposes; GC pauses would surface as dropped keys. | +| **TinyGo** | Modern, ergonomic. | ESP32-S3 support is thinner than Rust's; smaller ecosystem of embedded crates equivalents. | ### Decision + **Rust on `esp-idf-rs` (std).** It's the sweet spot: keeps the door open to the entire Rust ecosystem we need (`gitoxide` especially), gets us threads and TLS without writing them, and has Espressif as an actual upstream. ### Consequences + - Binary will be in the 1–2 MB range — comfortable in 16 MB flash. - Build times are real (clean build ~5–10 min). Acceptable. - Cross-compiling toolchain (`espup`) is one more thing to install. @@ -64,26 +67,29 @@ the binary-size / build-time costs traded against ecosystem access. **Scope:** Whole project. ### Context + We need a TUI-like editor (header, edit area, status, palettes later). The output medium is e-ink: pixel framebuffer with **partial-refresh windows** aligned to panel-internal regions, ~10× slower than an LCD per region. ### Options considered -| Option | Pros | Cons | -|---|---|---| -| **Ratatui** with a custom backend | Mature widget set, well-known API, lots of community examples. | Built for char-grid terminals over ANSI; per-cell diff fights e-ink's region-refresh model; backend would re-rasterise glyphs from cell-diffs; ~200 KB of binary and a leaky abstraction. | -| **Raw `embedded-graphics` only** | Smallest footprint, full control. | Every screen built from primitives; no widget reuse; status line / palette would each be ad-hoc. | -| **LVGL via Rust bindings** | Full GUI toolkit, themable. | Designed for actively-refreshing colour LCDs; e-ink integration is awkward; way more than we need. | -| **Custom thin widget layer on `embedded-graphics`** | Borrow Ratatui's API ideas (`Layout`, `Block`, `Paragraph`) without its rendering model; dirty-rect tracking aligned to e-ink regions; ~500 LoC. | We own and maintain the layer. | +| Option | Pros | Cons | +| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Ratatui** with a custom backend | Mature widget set, well-known API, lots of community examples. | Built for char-grid terminals over ANSI; per-cell diff fights e-ink's region-refresh model; backend would re-rasterise glyphs from cell-diffs; ~200 KB of binary and a leaky abstraction. | +| **Raw `embedded-graphics` only** | Smallest footprint, full control. | Every screen built from primitives; no widget reuse; status line / palette would each be ad-hoc. | +| **LVGL via Rust bindings** | Full GUI toolkit, themable. | Designed for actively-refreshing colour LCDs; e-ink integration is awkward; way more than we need. | +| **Custom thin widget layer on `embedded-graphics`** | Borrow Ratatui's API ideas (`Layout`, `Block`, `Paragraph`) without its rendering model; dirty-rect tracking aligned to e-ink regions; ~500 LoC. | We own and maintain the layer. | ### Decision -**Custom thin widget layer on `embedded-graphics`.** Steal the widget *API -shape* from Ratatui (because it's a good shape) but render directly to a + +**Custom thin widget layer on `embedded-graphics`.** Steal the widget _API +shape_ from Ratatui (because it's a good shape) but render directly to a pixel framebuffer with our own dirty-rectangle tracking sized to the panel's refresh regions. ### Consequences + - ~500 LoC of widget/layout code we maintain. Worth it. - We can tune refresh cadence (partial vs full) at the widget level. - If we later want to render to a terminal for desktop testing, we add a @@ -101,20 +107,22 @@ Owns the two top-ranked functions (H1 latency, H2 region area) in **Scope:** v0.1 through v1.0. 10.3" upgrade remains on the v1.x table. ### Context + The screen is the most user-facing hardware choice. It sets the aspect of the writing experience, the BOM cost, the GPIO budget, the framebuffer size, and the refresh feel. ### Options considered -| Option | Size / Res | Aspect | Pros | Cons | -|---|---|---|---|---| -| **GDEY0579T93 + DESPI-c579** | 5.79" / 792×272 | 2.9:1 strip | SPI, partial refresh, small framebuffer (~27 KB), Freewrite-style narrow viewport, low power, low GPIO use. | Only ~11 visible lines of edit area; less context on screen. | -| **Waveshare 7.5" V2** | 7.5" / 800×480 | 5:3 page | More lines visible, well-supported by `epd-waveshare` out of the box. | Bigger BOM, bigger framebuffer (~48 KB), more conventional / less typewriter-feeling. | -| **Waveshare 10.3" + IT8951** | 10.3" / 1872×1404 | 4:3 | Real "page" experience; great for long-form. | +$80 BOM; parallel bus eats GPIO; IT8951 adds a controller board; overkill for v0.1. | -| **2.9" / 4.2" smaller panels** | varied | varied | Cheap, common. | Too cramped for a typewriter; status bars eat the screen. | +| Option | Size / Res | Aspect | Pros | Cons | +| ------------------------------ | ----------------- | ----------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| **GDEY0579T93 + DESPI-c579** | 5.79" / 792×272 | 2.9:1 strip | SPI, partial refresh, small framebuffer (~27 KB), Freewrite-style narrow viewport, low power, low GPIO use. | Only ~11 visible lines of edit area; less context on screen. | +| **Waveshare 7.5" V2** | 7.5" / 800×480 | 5:3 page | More lines visible, well-supported by `epd-waveshare` out of the box. | Bigger BOM, bigger framebuffer (~48 KB), more conventional / less typewriter-feeling. | +| **Waveshare 10.3" + IT8951** | 10.3" / 1872×1404 | 4:3 | Real "page" experience; great for long-form. | +$80 BOM; parallel bus eats GPIO; IT8951 adds a controller board; overkill for v0.1. | +| **2.9" / 4.2" smaller panels** | varied | varied | Cheap, common. | Too cramped for a typewriter; status bars eat the screen. | ### Decision + **GDEY0579T93 driven over SPI via the DESPI-c579 breakout.** The strip aspect biases UX toward "current line + recent context" — the writing posture we actually want. Small framebuffer keeps PSRAM free for git pack @@ -122,6 +130,7 @@ data. The DESPI-c579 is a passive level-shifter / FPC adapter, not an active controller — same SPI driver model as any other e-paper. ### Consequences + - Visible edit area is ~11 lines. UI design must embrace this (no multi-pane, no large headers). See [v0.1 product → screen layout](v0.1-mvp-product.md#screen-layout). @@ -140,28 +149,31 @@ controller — same SPI driver model as any other e-paper. **Scope:** Whole project, all releases. ### Context + The device must do `add`, `commit`, `push` over the network. Optionally later: `fetch`, `pull`, `branch`. The library must compile against `esp-idf-rs` (std, mbedtls available). ### Options considered -| Option | Pros | Cons | -|---|---|---| -| **`libgit2-sys`** (C bindings) | Battle-tested, comprehensive, well-known. | C dependency complicates cross-compile to ESP32-S3; needs mbedtls glue; binary size; less Rust-idiomatic. | -| **`gitoxide` (`gix`)** | Pure Rust, modular crates (we only depend on what we use), idiomatic API, active development. | Smart-HTTP push path is newer than libgit2's; PSRAM allocation patterns less battle-tested on embedded. | -| **Hand-rolled HTTP + pack** | Smallest possible footprint. | Reinventing git internals; pack delta + ref discovery + index updates are not weekend work. | -| **Shell out to `git` binary** | Trivial. | There is no `git` binary on the ESP32-S3. | +| Option | Pros | Cons | +| ------------------------------ | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | +| **`libgit2-sys`** (C bindings) | Battle-tested, comprehensive, well-known. | C dependency complicates cross-compile to ESP32-S3; needs mbedtls glue; binary size; less Rust-idiomatic. | +| **`gitoxide` (`gix`)** | Pure Rust, modular crates (we only depend on what we use), idiomatic API, active development. | Smart-HTTP push path is newer than libgit2's; PSRAM allocation patterns less battle-tested on embedded. | +| **Hand-rolled HTTP + pack** | Smallest possible footprint. | Reinventing git internals; pack delta + ref discovery + index updates are not weekend work. | +| **Shell out to `git` binary** | Trivial. | There is no `git` binary on the ESP32-S3. | ### Decision + **`gitoxide`.** Modular means we pull only `gix-pack`, `gix-protocol`, `gix-transport`, etc. — not 200 KB of features we don't use. Pure Rust removes a class of cross-compile pain. The smart-HTTP path is validated in -spike 7 *before* we commit to integration; if it fails on the device, we +spike 7 _before_ we commit to integration; if it fails on the device, we fall back to `libgit2-sys` for v0.1 (documented as the kill-switch in the risk table). ### Consequences + - We become an early-ish embedded user of `gitoxide`; bugs reported back upstream. - Auth via PAT in an Authorization header — no SSH (see ADR-005). @@ -179,25 +191,28 @@ and [risks table](v0.1-mvp-technical.md#risks-and-how-well-know-they-bit-us). **Scope:** v0.1 through at least v0.9. ### Context + The device must authenticate to GitHub (or other git remotes) to push. Auth has to be: enterable on a tiny screen-less first-run flow, storable on-device, and reasonably secure for a personal appliance. ### Options considered -| Option | Pros | Cons | -|---|---|---| -| **HTTPS + PAT** | Trivial to implement; PAT is a string the user pastes during captive-portal setup; works with `gitoxide` smart-HTTP. | Long-lived secret on device; PAT rotation is manual. | -| **HTTPS + OAuth device flow** | No secret typed by hand; user approves on github.com. | Adds an OAuth client app to maintain; token still has to live on device; more first-run UX work. | -| **SSH** | No PAT; per-device deploy keys. | SSH on embedded is heavy (host-key handling, key generation); `gitoxide`'s SSH transport story is less mature than HTTPS; users would have to register the public key on GitHub anyway. | -| **GitHub App with installation token** | Strongest model, rotating credentials. | Massive overhead for a single-user device. | +| Option | Pros | Cons | +| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **HTTPS + PAT** | Trivial to implement; PAT is a string the user pastes during captive-portal setup; works with `gitoxide` smart-HTTP. | Long-lived secret on device; PAT rotation is manual. | +| **HTTPS + OAuth device flow** | No secret typed by hand; user approves on github.com. | Adds an OAuth client app to maintain; token still has to live on device; more first-run UX work. | +| **SSH** | No PAT; per-device deploy keys. | SSH on embedded is heavy (host-key handling, key generation); `gitoxide`'s SSH transport story is less mature than HTTPS; users would have to register the public key on GitHub anyway. | +| **GitHub App with installation token** | Strongest model, rotating credentials. | Massive overhead for a single-user device. | ### Decision + **HTTPS + PAT.** Stored in internal LittleFS, encrypted with a key derived from the chip's eFuse so a stolen SD card alone is not enough. Captive portal accepts the PAT during first-run setup. ### Consequences + - The user must generate a PAT with `repo` scope. Documented in [v0.1 product → first-run flow](v0.1-mvp-product.md#first-run-provisioning-flow). - PAT is never logged. Validated in code review. @@ -214,25 +229,28 @@ portal accepts the PAT during first-run setup. **Scope:** v0.1 through at least v1.0. ### Context + The firmware has several concurrent concerns: USB input, Wi-Fi maintenance, screen rendering, occasional git operations. None of them are I/O-bound at the scale where async wins. The number of "tasks" is bounded and small (≤ 8). ### Options considered -| Option | Pros | Cons | -|---|---|---| -| **`std::thread` + channels** | Boring, debuggable, stack traces work, no executor to tune; ESP-IDF FreeRTOS underneath is well-understood. | Each thread costs 8–32 KB stack depending on workload; not zero-cost like async. | -| **`embassy` async** | Trendy, ergonomic, low memory per task. | `esp-idf-rs` and `embassy` don't mix cleanly; adopting embassy means dropping `std` and rewriting against `esp-hal` (ADR-001 reversed). | -| **`tokio` on `esp-idf-rs`** | Familiar async. | Heavy executor, oversized for ≤ 8 tasks, mbedtls/`gitoxide` integration would need a lot of glue. | -| **Single-threaded event loop** | Smallest memory. | Long-running ops (git push, full refresh) block input. | +| Option | Pros | Cons | +| ------------------------------ | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| **`std::thread` + channels** | Boring, debuggable, stack traces work, no executor to tune; ESP-IDF FreeRTOS underneath is well-understood. | Each thread costs 8–32 KB stack depending on workload; not zero-cost like async. | +| **`embassy` async** | Trendy, ergonomic, low memory per task. | `esp-idf-rs` and `embassy` don't mix cleanly; adopting embassy means dropping `std` and rewriting against `esp-hal` (ADR-001 reversed). | +| **`tokio` on `esp-idf-rs`** | Familiar async. | Heavy executor, oversized for ≤ 8 tasks, mbedtls/`gitoxide` integration would need a lot of glue. | +| **Single-threaded event loop** | Smallest memory. | Long-running ops (git push, full refresh) block input. | ### Decision + **`std::thread` + `crossbeam-channel`.** Five tasks (`usb`, `wifi`, `ui`, `render`, `git`). Editor state behind a single `Mutex`. No `await`, no runtime to tune, no colour-of-functions problem. ### Consequences + - ~76 KB of stack space across the five task stacks (8 + 8 + 16 + 12 + 32 KB — see [v0.1 technical → threads / tasks](v0.1-mvp-technical.md#threads--tasks) for the breakdown). Comfortable in the ESP32-S3's 512 KB internal SRAM. @@ -248,25 +266,28 @@ runtime to tune, no colour-of-functions problem. **Scope:** Whole project. ### Context + Two storage needs: a large, removable, growable area for the git working copy and notes; and a small, durable, never-removed area for device config (Wi-Fi credentials, PAT, remote URL). ### Options considered -| Option | Pros | Cons | -|---|---|---| -| **SD (FAT) for working copy + LittleFS (internal) for config** | Plays to each medium's strengths; user can pop the SD to read on desktop; config can't be lost by yanking the card. | Two filesystems to manage. | -| **All on SD** | One filesystem. | Config disappears if SD is removed; PAT on FAT is harder to protect than on encrypted NVS. | -| **All in internal flash** | Single medium; encrypted. | 16 MB flash limits notes growth; no desktop-side access; SD slot becomes pointless. | -| **SPIFFS for everything** | Single FS, well-known on ESP32. | SPIFFS isn't great with large files; no removability. | +| Option | Pros | Cons | +| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| **SD (FAT) for working copy + LittleFS (internal) for config** | Plays to each medium's strengths; user can pop the SD to read on desktop; config can't be lost by yanking the card. | Two filesystems to manage. | +| **All on SD** | One filesystem. | Config disappears if SD is removed; PAT on FAT is harder to protect than on encrypted NVS. | +| **All in internal flash** | Single medium; encrypted. | 16 MB flash limits notes growth; no desktop-side access; SD slot becomes pointless. | +| **SPIFFS for everything** | Single FS, well-known on ESP32. | SPIFFS isn't great with large files; no removability. | ### Decision + **FAT on SD for `/sd/repo/` and `/sd/local/`. LittleFS on internal flash for `/nvs/config.toml`.** PAT inside config is encrypted with an eFuse- derived key. ### Consequences + - User can plug the SD into a laptop and read/edit files there. Discouraged but possible. - Config survives SD reformatting. @@ -283,6 +304,7 @@ derived key. **Scope:** v0.1 only. Revisited in ADR-future at v0.8. ### Context + "DIY typewriter" suggests portability, which suggests battery. But battery adds: charging circuit, BMS, thermal margin, soft power switch, lid-close detection, sleep states. Each of those has its own bring-up cost. @@ -296,11 +318,13 @@ detection, sleep states. Each of those has its own bring-up cost. - **LiPo + dedicated charger IC + buck/boost.** More control, more parts. ### Decision + **Wall power only for v0.1.** Battery is its own phase (v0.8) once the power profile of "boot + type + idle + push" is measured on real hardware. Sizing a battery before measuring is guessing. ### Consequences + - v0.1 device is tethered. Not the final aesthetic, but the right MVP — scope is in [v0.1 product → out of scope](v0.1-mvp-product.md#out-of-scope-for-v01). - We can decide cell capacity from real numbers in v0.8, not specs sheets. @@ -315,23 +339,26 @@ Sizing a battery before measuring is guessing. **Scope:** v0.1 through at least v1.0. ### Context + The Nuphy keyboard speaks both wired USB-C (HID) and Bluetooth LE (HID). The ESP32-S3 has USB OTG (host capable) and BLE 5. Either transport works. ### Options considered -| Option | Pros | Cons | -|---|---|---| -| **USB host (TinyUSB)** | Keyboard draws no battery of its own; ESP32-S3 powers it through the host port; standard boot-protocol HID is well-supported; no radio contention with Wi-Fi during push. | One more USB connector on the enclosure; cable between device and keyboard (or shared chassis). | -| **BLE-HID** | No cable; keyboard can be slightly remote from the device. | Keyboard has its own battery to manage; BLE shares the 2.4 GHz radio with Wi-Fi, so a `Ctrl-G` push contends with input; pairing UX is more first-run work. | -| **UART receiver (custom keyboard firmware)** | Lowest latency, simplest stack. | Requires reflashing the Nuphy or building a passthrough; not viable as a product choice. | +| Option | Pros | Cons | +| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **USB host (TinyUSB)** | Keyboard draws no battery of its own; ESP32-S3 powers it through the host port; standard boot-protocol HID is well-supported; no radio contention with Wi-Fi during push. | One more USB connector on the enclosure; cable between device and keyboard (or shared chassis). | +| **BLE-HID** | No cable; keyboard can be slightly remote from the device. | Keyboard has its own battery to manage; BLE shares the 2.4 GHz radio with Wi-Fi, so a `Ctrl-G` push contends with input; pairing UX is more first-run work. | +| **UART receiver (custom keyboard firmware)** | Lowest latency, simplest stack. | Requires reflashing the Nuphy or building a passthrough; not viable as a product choice. | ### Decision + **USB host (TinyUSB) for v0.1.** BLE-HID is kept as a documented fallback if TinyUSB host turns out unstable ([spike 4](v0.1-mvp-technical.md#hardware-bring-up-order) is the gate). ### Consequences + - Enclosure design must include a USB-A or USB-C port for the keyboard. - The Nuphy's own battery is irrelevant when wired — saves the user a charging surface. diff --git a/docs/qfd.md b/docs/qfd.md index e0b6fd6..02d7aa5 100644 --- a/docs/qfd.md +++ b/docs/qfd.md @@ -1,7 +1,7 @@ # Quality Function Deployment -Translates what the device must *be* (user-facing requirements) into what it -must *do* (engineering functions) and what we must *build* (components). +Translates what the device must _be_ (user-facing requirements) into what it +must _do_ (engineering functions) and what we must _build_ (components). Surfaces the few targets that dominate the design and the conflicts between them. Every decision cell points back to [`adr.md`](adr.md). @@ -21,20 +21,20 @@ weights: **9** strong, **3** medium, **1** weak, blank none. What a user (= me) values about the device, with importance weights on a 1–10 scale. Source columns point at the doc the requirement comes from. -| ID | Requirement | Weight | Source | -|-----|---------------------------------------------------|:------:|--------| -| W1 | Sub-second visible response to typing | 10 | [product → story 2](v0.1-mvp-product.md#user-stories), [README → UX](../README.md#ux-boundaries-set-by-the-medium) | -| W2 | `Ctrl-G` reliably lands a commit on GitHub | 9 | [product → story 4](v0.1-mvp-product.md#user-stories) | -| W3 | Pulling power never corrupts the file | 10 | [product → story 5](v0.1-mvp-product.md#user-stories), [acceptance](v0.1-mvp-product.md#acceptance-criteria) | -| W4 | One-shot first-run setup, never repeated | 7 | [product → story 1](v0.1-mvp-product.md#user-stories) | -| W5 | Quick boot to a writing cursor | 6 | [product → acceptance](v0.1-mvp-product.md#acceptance-criteria) (≤ 5 s) | -| W6 | Long sessions without crash / lag / drift | 9 | [product → acceptance](v0.1-mvp-product.md#acceptance-criteria) (1 h soak) | -| W7 | Distraction-free, single-purpose surface | 8 | [README → vision](../README.md#vision) | -| W8 | E-ink-honest UI (no blink, no animation, no flash spam) | 7 | [README → UX](../README.md#ux-boundaries-set-by-the-medium) | -| W9 | Refactorable across nine downstream releases | 8 | [roadmap](roadmap.md) | -| W10 | Hackable / DIY-shaped BOM and code | 5 | [README → vision](../README.md#vision) | -| W11 | Multi-day battery life (v0.8 onward) | 4 | [roadmap → v0.8](roadmap.md#v08--power-battery--sleep--) | -| W12 | Local-only file scope coexists with git scope (v0.5+) | 5 | [README → scopes](../README.md#vision), [roadmap → v0.5](roadmap.md#v05--file-palette--multi-file--) | +| ID | Requirement | Weight | Source | +| --- | ------------------------------------------------------- | :----: | ------------------------------------------------------------------------------------------------------------------ | +| W1 | Sub-second visible response to typing | 10 | [product → story 2](v0.1-mvp-product.md#user-stories), [README → UX](../README.md#ux-boundaries-set-by-the-medium) | +| W2 | `Ctrl-G` reliably lands a commit on GitHub | 9 | [product → story 4](v0.1-mvp-product.md#user-stories) | +| W3 | Pulling power never corrupts the file | 10 | [product → story 5](v0.1-mvp-product.md#user-stories), [acceptance](v0.1-mvp-product.md#acceptance-criteria) | +| W4 | One-shot first-run setup, never repeated | 7 | [product → story 1](v0.1-mvp-product.md#user-stories) | +| W5 | Quick boot to a writing cursor | 6 | [product → acceptance](v0.1-mvp-product.md#acceptance-criteria) (≤ 5 s) | +| W6 | Long sessions without crash / lag / drift | 9 | [product → acceptance](v0.1-mvp-product.md#acceptance-criteria) (1 h soak) | +| W7 | Distraction-free, single-purpose surface | 8 | [README → vision](../README.md#vision) | +| W8 | E-ink-honest UI (no blink, no animation, no flash spam) | 7 | [README → UX](../README.md#ux-boundaries-set-by-the-medium) | +| W9 | Refactorable across nine downstream releases | 8 | [roadmap](roadmap.md) | +| W10 | Hackable / DIY-shaped BOM and code | 5 | [README → vision](../README.md#vision) | +| W11 | Multi-day battery life (v0.8 onward) | 4 | [roadmap → v0.8](roadmap.md#v08--power-battery--sleep--) | +| W12 | Local-only file scope coexists with git scope (v0.5+) | 5 | [README → scopes](../README.md#vision), [roadmap → v0.5](roadmap.md#v05--file-palette--multi-file--) | --- @@ -43,23 +43,23 @@ What a user (= me) values about the device, with importance weights on a Measurable characteristics. Targets are v0.1 unless noted. Direction column shows what "better" looks like (↑ higher, ↓ lower, → fixed). -| ID | Function | Dir | v0.1 target | v1.0 target | -|-----|-------------------------------------------------------|:---:|--------------------------|-----------------------| -| H1 | Keypress → glyph latency | ↓ | ≤ 200 ms | ≤ 150 ms | -| H2 | Partial-refresh region area per keystroke | ↓ | ≤ 1 text line (~22 px h) | same | -| H3 | Full-refresh cadence (clears ghosting) | → | 1 per 20 partials | tuned by panel temp | -| H4 | Cold boot → cursor ready | ↓ | ≤ 5 s | ≤ 3 s | -| H5 | Continuous-typing endurance (no drop, no leak) | ↑ | ≥ 1 h | ≥ 8 h | -| H6 | `Ctrl-G` push success rate on healthy Wi-Fi | ↑ | ≥ 95 % | ≥ 99 % | -| H7 | Push end-to-end (one-file commit) | ↓ | ≤ 30 s | ≤ 10 s | -| H8 | Save survives power loss after status confirms | → | 100 % | 100 % | -| H9 | PSRAM heap headroom during push | ↑ | ≥ 1 MB free at peak | same | -| H10 | Firmware binary size | ↓ | ≤ 2 MB | ≤ 1.5 MB | -| H11 | Stack budget across all tasks | ↓ | ≤ 80 KB (sum) | same | -| H12 | Wi-Fi reconnect on transient outage | ↓ | ≤ 30 s | ≤ 10 s | -| H13 | Idle / typing / push current draw | ↓ | measured only | sized for >2 days | -| H14 | Module count / public-API surface (refactor proxy) | → | ≤ 8 modules | same | -| H15 | Build time (clean, release) | ↓ | ≤ 10 min | ≤ 7 min | +| ID | Function | Dir | v0.1 target | v1.0 target | +| --- | -------------------------------------------------- | :-: | ------------------------ | ------------------- | +| H1 | Keypress → glyph latency | ↓ | ≤ 200 ms | ≤ 150 ms | +| H2 | Partial-refresh region area per keystroke | ↓ | ≤ 1 text line (~22 px h) | same | +| H3 | Full-refresh cadence (clears ghosting) | → | 1 per 20 partials | tuned by panel temp | +| H4 | Cold boot → cursor ready | ↓ | ≤ 5 s | ≤ 3 s | +| H5 | Continuous-typing endurance (no drop, no leak) | ↑ | ≥ 1 h | ≥ 8 h | +| H6 | `Ctrl-G` push success rate on healthy Wi-Fi | ↑ | ≥ 95 % | ≥ 99 % | +| H7 | Push end-to-end (one-file commit) | ↓ | ≤ 30 s | ≤ 10 s | +| H8 | Save survives power loss after status confirms | → | 100 % | 100 % | +| H9 | PSRAM heap headroom during push | ↑ | ≥ 1 MB free at peak | same | +| H10 | Firmware binary size | ↓ | ≤ 2 MB | ≤ 1.5 MB | +| H11 | Stack budget across all tasks | ↓ | ≤ 80 KB (sum) | same | +| H12 | Wi-Fi reconnect on transient outage | ↓ | ≤ 30 s | ≤ 10 s | +| H13 | Idle / typing / push current draw | ↓ | measured only | sized for >2 days | +| H14 | Module count / public-API surface (refactor proxy) | → | ≤ 8 modules | same | +| H15 | Build time (clean, release) | ↓ | ≤ 10 min | ≤ 7 min | --- @@ -69,21 +69,21 @@ Reading: row × column cell is how strongly the function (H) advances the requirement (W). Importance at the bottom is `Σ(weight × strength)` — the weighted vote on which functions deserve the most engineering attention. -| | H1 lat | H2 area | H3 cad | H4 boot | H5 soak | H6 push% | H7 push s | H8 dura | H9 heap | H10 bin | H11 stk | H12 wifi | H13 mA | H14 mod | H15 build | -|-------|:------:|:-------:|:------:|:-------:|:-------:|:--------:|:---------:|:-------:|:-------:|:-------:|:-------:|:--------:|:------:|:-------:|:---------:| -| W1 (10) | **9** | 9 | 3 | | 3 | | | | 1 | | 1 | | | | | -| W2 (9) | | | | | | **9** | 3 | | 9 | | | 9 | | | | -| W3 (10) | | | | | | | | **9** | | | | | | | | -| W4 (7) | | | | | | 3 | | | | | | 3 | | 1 | | -| W5 (6) | | | | **9** | | | | | | 3 | | | | | | -| W6 (9) | 3 | | 3 | | **9** | 3 | | 3 | 9 | | 3 | 3 | | | | -| W7 (8) | 3 | 3 | 3 | | | | | | | | | | 3 | 1 | | -| W8 (7) | 1 | 9 | **9** | | | | | | | | | | | | | -| W9 (8) | | | | | | | | | | 1 | 1 | | | **9** | 3 | -| W10 (5) | | | | | | | | | | 3 | | | 1 | 3 | 1 | -| W11 (4) | | | | | | | | | | | | | **9** | | | -| W12 (5) | | | | | | 1 | | 3 | | | | | | 3 | | -| **Σ** | **138**| **174** | **126**| **54** | **108** | **132** | **27** | **147** | **162** | **38** | **41** | **120** | **74** | **115** | **29** | +| | H1 lat | H2 area | H3 cad | H4 boot | H5 soak | H6 push% | H7 push s | H8 dura | H9 heap | H10 bin | H11 stk | H12 wifi | H13 mA | H14 mod | H15 build | +| ------- | :-----: | :-----: | :-----: | :-----: | :-----: | :------: | :-------: | :-----: | :-----: | :-----: | :-----: | :------: | :----: | :-----: | :-------: | +| W1 (10) | **9** | 9 | 3 | | 3 | | | | 1 | | 1 | | | | | +| W2 (9) | | | | | | **9** | 3 | | 9 | | | 9 | | | | +| W3 (10) | | | | | | | | **9** | | | | | | | | +| W4 (7) | | | | | | 3 | | | | | | 3 | | 1 | | +| W5 (6) | | | | **9** | | | | | | 3 | | | | | | +| W6 (9) | 3 | | 3 | | **9** | 3 | | 3 | 9 | | 3 | 3 | | | | +| W7 (8) | 3 | 3 | 3 | | | | | | | | | | 3 | 1 | | +| W8 (7) | 1 | 9 | **9** | | | | | | | | | | | | | +| W9 (8) | | | | | | | | | | 1 | 1 | | | **9** | 3 | +| W10 (5) | | | | | | | | | | 3 | | | 1 | 3 | 1 | +| W11 (4) | | | | | | | | | | | | | **9** | | | +| W12 (5) | | | | | | 1 | | 3 | | | | | | 3 | | +| **Σ** | **138** | **174** | **126** | **54** | **108** | **132** | **27** | **147** | **162** | **38** | **41** | **120** | **74** | **115** | **29** | ### Top engineering priorities (from importance) @@ -114,7 +114,7 @@ The roof shows where pushing one function pushes another the wrong way. **`– –`** strong conflict. | | H1 | H2 | H3 | H4 | H5 | H6 | H7 | H8 | H9 | H10 | H11 | H12 | H13 | H14 | H15 | -|---------|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----| +| ------- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | **H1** | — | ++ | – | | + | | | | | | | | – | | | | **H2** | | — | ++ | | | | | | | | | | + | | | | **H3** | | | — | | | | | | | | | | + | | | @@ -137,7 +137,7 @@ The roof shows where pushing one function pushes another the wrong way. second pile up ghosting faster, demanding earlier full refreshes — visible flashes that hurt H8 perception and H1 burst behaviour. The ADR-003 strip aspect is the structural answer: a small framebuffer makes - *both* cheaper, not one at the expense of the other. The runtime answer + _both_ cheaper, not one at the expense of the other. The runtime answer is render §H3: schedule full refreshes on idle ≥ 1 s (v0.1 tech doc). - **H9 heap ↔ H10 binary size** (strong). std + gitoxide + mbedtls inflate both. We chose to spend on these (ADR-001, ADR-004) because 16 MB flash @@ -170,44 +170,44 @@ constrains the choice. Components (with anchoring ADR): -| ID | Component | ADR | -|-----|----------------------------------------------|------------| -| C1 | ESP32-S3-N16R8 SoC | ADR-001, ADR-008 | -| C2 | `esp-idf-rs` (std) + ESP-IDF | ADR-001 | -| C3 | `std::thread` + `crossbeam-channel` | ADR-006 | -| C4 | PSRAM allocator wrapper | ADR-001 | -| C5 | GDEY0579T93 + DESPI-c579 panel | ADR-003 | -| C6 | `embedded-graphics` + e-paper driver | ADR-002, ADR-003 | -| C7 | Custom widget / dirty-rect layer | ADR-002 | -| C8 | `ropey` rope buffer | ADR-001 (ecosystem) | -| C9 | TinyUSB host (`esp-idf` bindings) | ADR-009 | -| C10 | FAT on microSD | ADR-007 | -| C11 | LittleFS on internal flash | ADR-007 | -| C12 | `gitoxide` (`gix-*`) | ADR-004 | -| C13 | mbedtls TLS (via ESP-IDF) | ADR-005 | -| C14 | HTTPS + GitHub PAT auth | ADR-005 | -| C15 | eFuse-derived encryption key | ADR-005, ADR-007 | -| C16 | USB-C wall PSU | ADR-008 | +| ID | Component | ADR | +| --- | ------------------------------------ | ------------------- | +| C1 | ESP32-S3-N16R8 SoC | ADR-001, ADR-008 | +| C2 | `esp-idf-rs` (std) + ESP-IDF | ADR-001 | +| C3 | `std::thread` + `crossbeam-channel` | ADR-006 | +| C4 | PSRAM allocator wrapper | ADR-001 | +| C5 | GDEY0579T93 + DESPI-c579 panel | ADR-003 | +| C6 | `embedded-graphics` + e-paper driver | ADR-002, ADR-003 | +| C7 | Custom widget / dirty-rect layer | ADR-002 | +| C8 | `ropey` rope buffer | ADR-001 (ecosystem) | +| C9 | TinyUSB host (`esp-idf` bindings) | ADR-009 | +| C10 | FAT on microSD | ADR-007 | +| C11 | LittleFS on internal flash | ADR-007 | +| C12 | `gitoxide` (`gix-*`) | ADR-004 | +| C13 | mbedtls TLS (via ESP-IDF) | ADR-005 | +| C14 | HTTPS + GitHub PAT auth | ADR-005 | +| C15 | eFuse-derived encryption key | ADR-005, ADR-007 | +| C16 | USB-C wall PSU | ADR-008 | Function-to-component matrix (9 strong / 3 medium / 1 weak): -| | C1 SoC | C2 std | C3 thr | C4 PSR | C5 EPD | C6 eg | C7 wid | C8 rope | C9 USB | C10 SD | C11 LFS | C12 gix | C13 TLS | C14 PAT | C15 efs | C16 PSU | -|-------|:------:|:------:|:------:|:------:|:------:|:-----:|:------:|:-------:|:------:|:------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:| -| H1 lat | 3 | 1 | 9 | 3 | 9 | 9 | 9 | 3 | 9 | | | | | | | | -| H2 area | | | | | 9 | 9 | 9 | | | | | | | | | | -| H3 cad | | | | | 9 | 3 | 9 | | | | | | | | | | -| H4 boot | 3 | 9 | 3 | 1 | 3 | | | | | 9 | 3 | | | | | | -| H5 soak | 3 | 3 | 3 | 9 | 1 | | | 9 | 9 | 3 | | 3 | 3 | | | | -| H6 push% | | 3 | | | | | | | | | | 9 | 9 | 9 | | | -| H7 push s| | | 3 | 1 | | | | | | 3 | | 9 | 9 | | | | -| H8 dura | | 3 | | | | | | | | 9 | 9 | | | | | | -| H9 heap | 3 | 3 | | 9 | | | | 3 | | | | 9 | 9 | | | | -| H10 bin | | 9 | 1 | | | 3 | 3 | 3 | 3 | | | 9 | 3 | | | | -| H11 stk | | | 9 | | | | | | 3 | | | 3 | | | | | -| H12 wifi | 3 | 9 | | | | | | | | | | | 3 | | | | -| H13 mA | 9 | | 1 | | 9 | | | | 3 | 3 | | | | | | 9 | -| H14 mod | | 3 | 3 | | | 3 | 9 | 3 | | | | 9 | | | | | -| H15 build| | 9 | | | | | | | | | | 9 | 3 | | | | +| | C1 SoC | C2 std | C3 thr | C4 PSR | C5 EPD | C6 eg | C7 wid | C8 rope | C9 USB | C10 SD | C11 LFS | C12 gix | C13 TLS | C14 PAT | C15 efs | C16 PSU | +| --------- | :----: | :----: | :----: | :----: | :----: | :---: | :----: | :-----: | :----: | :----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | +| H1 lat | 3 | 1 | 9 | 3 | 9 | 9 | 9 | 3 | 9 | | | | | | | | +| H2 area | | | | | 9 | 9 | 9 | | | | | | | | | | +| H3 cad | | | | | 9 | 3 | 9 | | | | | | | | | | +| H4 boot | 3 | 9 | 3 | 1 | 3 | | | | | 9 | 3 | | | | | | +| H5 soak | 3 | 3 | 3 | 9 | 1 | | | 9 | 9 | 3 | | 3 | 3 | | | | +| H6 push% | | 3 | | | | | | | | | | 9 | 9 | 9 | | | +| H7 push s | | | 3 | 1 | | | | | | 3 | | 9 | 9 | | | | +| H8 dura | | 3 | | | | | | | | 9 | 9 | | | | | | +| H9 heap | 3 | 3 | | 9 | | | | 3 | | | | 9 | 9 | | | | +| H10 bin | | 9 | 1 | | | 3 | 3 | 3 | 3 | | | 9 | 3 | | | | +| H11 stk | | | 9 | | | | | | 3 | | | 3 | | | | | +| H12 wifi | 3 | 9 | | | | | | | | | | | 3 | | | | +| H13 mA | 9 | | 1 | | 9 | | | | 3 | 3 | | | | | | 9 | +| H14 mod | | 3 | 3 | | | 3 | 9 | 3 | | | | 9 | | | | | +| H15 build | | 9 | | | | | | | | | | 9 | 3 | | | | ### Read across, not down @@ -219,7 +219,7 @@ Function-to-component matrix (9 strong / 3 medium / 1 weak): `libgit2-sys` if spike 7 fails). It's also why H9 sits in the top three priorities — `gitoxide`'s memory profile is the unknown. - **C2** (std runtime) sits underneath almost everything, but it's the - *enabler* (H4 boot, H10 binary, H12 Wi-Fi) rather than the bottleneck. + _enabler_ (H4 boot, H10 binary, H12 Wi-Fi) rather than the bottleneck. Reversing ADR-001 would force re-deciding ADR-004, ADR-005, ADR-006, ADR-007 all at once — they're a single decision in three drawers. @@ -230,21 +230,21 @@ Function-to-component matrix (9 strong / 3 medium / 1 weak): Pulled from §3 importance and §4 conflicts, in priority order. These are the numbers spikes 2–7 must validate before integration starts. -| Rank | Function | Target | Watched on | If we miss it | -|------|----------------|----------------------|-----------------------|-----------------------------------------------| -| 1 | H2 region area | ≤ 1 line per keypress| spike 2 + spike 5 | Increase font size to shrink per-glyph dirty rect (ADR-003 consequence) | -| 2 | H9 PSRAM heap | ≥ 1 MB free at push peak | spike 7 | ADR-004 kill-switch → `libgit2-sys`; cap rope at 128 KB | -| 3 | H8 durability | 100 % survive power yank after status | bench HIL | Re-evaluate ADR-007 (move config to internal NVS only) | -| 4 | H1 latency | ≤ 200 ms keypress→glyph | spike 5 | Larger partial-refresh region; render multi-char bursts | -| 5 | H6 push % | ≥ 95 % on healthy Wi-Fi | spike 6 + spike 7 | TLS cipher trim; reconnect backoff tuning | -| 6 | H3 cadence | full every ~20 partials | spike 2 | Adjust per panel temperature; defer flash to idle ≥ 1 s | -| 7 | H4 boot | ≤ 5 s to cursor | integration smoke | Trim startup logging; lazy-mount SD after splash | -| 8 | H5 soak | 1 h no leak / no drop| 1 h bench soak | Glyph-cache eviction; PSRAM heap-fragmentation review | +| Rank | Function | Target | Watched on | If we miss it | +| ---- | -------------- | ------------------------------------- | ----------------- | ----------------------------------------------------------------------- | +| 1 | H2 region area | ≤ 1 line per keypress | spike 2 + spike 5 | Increase font size to shrink per-glyph dirty rect (ADR-003 consequence) | +| 2 | H9 PSRAM heap | ≥ 1 MB free at push peak | spike 7 | ADR-004 kill-switch → `libgit2-sys`; cap rope at 128 KB | +| 3 | H8 durability | 100 % survive power yank after status | bench HIL | Re-evaluate ADR-007 (move config to internal NVS only) | +| 4 | H1 latency | ≤ 200 ms keypress→glyph | spike 5 | Larger partial-refresh region; render multi-char bursts | +| 5 | H6 push % | ≥ 95 % on healthy Wi-Fi | spike 6 + spike 7 | TLS cipher trim; reconnect backoff tuning | +| 6 | H3 cadence | full every ~20 partials | spike 2 | Adjust per panel temperature; defer flash to idle ≥ 1 s | +| 7 | H4 boot | ≤ 5 s to cursor | integration smoke | Trim startup logging; lazy-mount SD after splash | +| 8 | H5 soak | 1 h no leak / no drop | 1 h bench soak | Glyph-cache eviction; PSRAM heap-fragmentation review | The two not-in-MVP rows but already-shaped-by-design: -| — | H13 current | Measured only in v0.1 | bench multimeter | Cell sizing for v0.8 is data-driven, not spec-sheet | -| — | H11 stacks | Sum ≤ 80 KB | static analysis | Was off-by-2x in ADR-006 pre-fix — corrected in §7 | +| — | H13 current | Measured only in v0.1 | bench multimeter | Cell sizing for v0.8 is data-driven, not spec-sheet | +| — | H11 stacks | Sum ≤ 80 KB | static analysis | Was off-by-2x in ADR-006 pre-fix — corrected in §7 | --- @@ -252,19 +252,19 @@ The two not-in-MVP rows but already-shaped-by-design: Plain-language summary of what we accepted in exchange for what. -| Tradeoff | Got | Paid | ADR | -|-------------------------------------------------------|-------------------------------|-------------------------------|---------| -| std (esp-idf-rs) over no_std (esp-hal) | Heap, threads, VFS, mbedtls, gitoxide-compatible | +1 MB binary, +5–10 min builds | ADR-001 | -| Custom widget layer over Ratatui | Dirty-rects aligned to e-ink regions; 200 KB binary back | 500 LoC we own and maintain | ADR-002 | -| 5.79" strip panel over 7.5" page or 10.3" reader | 27 KB framebuffer, fast partial refresh, "Freewrite" UX | Only ~11 visible lines | ADR-003 | -| `gitoxide` over `libgit2-sys` | Pure Rust, modular, no FFI cross-compile pain | Smart-HTTP path is newer; PSRAM profile unproven (spike 7) | ADR-004 | -| HTTPS + PAT over OAuth device-flow or SSH | First-run UX fits in a captive-portal form | Long-lived secret on device; manual rotation in v0.1 | ADR-005 | -| `std::thread` over `embassy` or `tokio` | Boring, debuggable, real stack traces; no exec to tune | ~76 KB total stack across 5 tasks | ADR-006 | -| FAT-on-SD + LittleFS-on-flash split | Desktop can read SD; config survives SD reformat | Two filesystems to manage; FAT's power-loss weakness mitigated by atomic-rename | ADR-007 | -| Wall power for v0.1, battery deferred | Measure real draw before sizing the cell | Tethered MVP; not the final aesthetic | ADR-008 | -| USB host (TinyUSB) over BLE-HID | No radio contention with Wi-Fi during push; keyboard powered from the device | One more USB connector on enclosure | ADR-009 | +| Tradeoff | Got | Paid | ADR | +| ------------------------------------------------ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------- | +| std (esp-idf-rs) over no_std (esp-hal) | Heap, threads, VFS, mbedtls, gitoxide-compatible | +1 MB binary, +5–10 min builds | ADR-001 | +| Custom widget layer over Ratatui | Dirty-rects aligned to e-ink regions; 200 KB binary back | 500 LoC we own and maintain | ADR-002 | +| 5.79" strip panel over 7.5" page or 10.3" reader | 27 KB framebuffer, fast partial refresh, "Freewrite" UX | Only ~11 visible lines | ADR-003 | +| `gitoxide` over `libgit2-sys` | Pure Rust, modular, no FFI cross-compile pain | Smart-HTTP path is newer; PSRAM profile unproven (spike 7) | ADR-004 | +| HTTPS + PAT over OAuth device-flow or SSH | First-run UX fits in a captive-portal form | Long-lived secret on device; manual rotation in v0.1 | ADR-005 | +| `std::thread` over `embassy` or `tokio` | Boring, debuggable, real stack traces; no exec to tune | ~76 KB total stack across 5 tasks | ADR-006 | +| FAT-on-SD + LittleFS-on-flash split | Desktop can read SD; config survives SD reformat | Two filesystems to manage; FAT's power-loss weakness mitigated by atomic-rename | ADR-007 | +| Wall power for v0.1, battery deferred | Measure real draw before sizing the cell | Tethered MVP; not the final aesthetic | ADR-008 | +| USB host (TinyUSB) over BLE-HID | No radio contention with Wi-Fi during push; keyboard powered from the device | One more USB connector on enclosure | ADR-009 | -### Conflicts left explicitly *unresolved* by v0.1 +### Conflicts left explicitly _unresolved_ by v0.1 These are the live tensions we are watching, not deciding harder: @@ -304,7 +304,7 @@ and is not load-bearing. - When a new ADR lands, add its components to §5 and re-score any function-row whose dominant component changed. - When a spike returns numbers, update §6's "Target" or "Watched on" - columns — this is the doc that *should* feel out of date if measured + columns — this is the doc that _should_ feel out of date if measured reality drifts from estimates. - The WHATs change rarely; the HOWs change with each release; the matrices are recomputed when either side changes. diff --git a/docs/v0.1-mvp-product.md b/docs/v0.1-mvp-product.md index a391172..7f3c01d 100644 --- a/docs/v0.1-mvp-product.md +++ b/docs/v0.1-mvp-product.md @@ -147,14 +147,14 @@ don't fight it. ## Error UX -| Failure | What the user sees | -|---|---| -| No SD card on boot | "no SD card — insert one and reboot" | -| SD card mounted but `/sd/repo` missing | drop back into first-run setup | -| Wi-Fi won't connect | status shows `Wi-Fi ✗`; editing still works; `Ctrl-G` shows "no network" | -| `Ctrl-G` push rejected (auth, non-fast-forward, etc.) | status shows reason; commit stays local; user can retry | -| File write fails | status flashes "save failed"; buffer stays dirty | -| Keyboard disconnects | header shows `⌨ ✗`; editing pauses; reconnects automatically | +| Failure | What the user sees | +| ----------------------------------------------------- | ------------------------------------------------------------------------ | +| No SD card on boot | "no SD card — insert one and reboot" | +| SD card mounted but `/sd/repo` missing | drop back into first-run setup | +| Wi-Fi won't connect | status shows `Wi-Fi ✗`; editing still works; `Ctrl-G` shows "no network" | +| `Ctrl-G` push rejected (auth, non-fast-forward, etc.) | status shows reason; commit stays local; user can retry | +| File write fails | status flashes "save failed"; buffer stays dirty | +| Keyboard disconnects | header shows `⌨ ✗`; editing pauses; reconnects automatically | No modal dialogs. Errors live in the status line. The editor is never blocked from accepting keystrokes (except during the partial-refresh frame itself). @@ -199,7 +199,7 @@ place to check before declaring an item done. ## Non-goals as success criteria -These are deliberately *not* measured in v0.1: +These are deliberately _not_ measured in v0.1: - Boot time below 3 s (Phase 1 target). - Battery life (no battery yet). diff --git a/docs/v0.1-mvp-technical.md b/docs/v0.1-mvp-technical.md index 1b52a38..690fbed 100644 --- a/docs/v0.1-mvp-technical.md +++ b/docs/v0.1-mvp-technical.md @@ -42,13 +42,13 @@ blocked by I/O. ### Threads / tasks -| Task | Core | Stack | Responsibility | -|---|---|---|---| -| `usb_task` | 0 | 8 KB | TinyUSB host loop, decode HID reports, post `KeyEvent`s | -| `wifi_task` | 0 | 8 KB | Provisioning AP or station mode; expose status | -| `ui_task` | 1 | 16 KB | Consume `KeyEvent`s, mutate editor state, enqueue render | -| `render_task` | 1 | 12 KB | Drain render queue, do partial/full refresh on EPD | -| `git_task` | 1 | 32 KB | Triggered by `Ctrl-G`; runs gitoxide commit + push | +| Task | Core | Stack | Responsibility | +| ------------- | ---- | ----- | -------------------------------------------------------- | +| `usb_task` | 0 | 8 KB | TinyUSB host loop, decode HID reports, post `KeyEvent`s | +| `wifi_task` | 0 | 8 KB | Provisioning AP or station mode; expose status | +| `ui_task` | 1 | 16 KB | Consume `KeyEvent`s, mutate editor state, enqueue render | +| `render_task` | 1 | 12 KB | Drain render queue, do partial/full refresh on EPD | +| `git_task` | 1 | 32 KB | Triggered by `Ctrl-G`; runs gitoxide commit + push | All inter-task communication is via `crossbeam-channel` or `std::sync::mpsc` bounded queues. The editor state is `Arc>`; the lock is @@ -117,15 +117,15 @@ stack decision (e.g. fall back to libgit2; switch to a separate SD SPI bus). ### `keymap` — v0.1 keymap table -| Key | Action | -|---|---| -| printable | insert char | -| `Backspace` | delete char before cursor | -| `Enter` | insert `\n` | +| Key | Action | +| --------------- | --------------------------------- | +| printable | insert char | +| `Backspace` | delete char before cursor | +| `Enter` | insert `\n` | | `←` `→` `↑` `↓` | move cursor (visual lines for ↑↓) | -| `Home` `End` | line start / end | -| `Ctrl-S` | save | -| `Ctrl-G` | save (if dirty) + commit + push | +| `Home` `End` | line start / end | +| `Ctrl-S` | save | +| `Ctrl-G` | save (if dirty) + commit + push | Anything else is ignored in v0.1. @@ -198,14 +198,14 @@ PSRAM heap during push is a top-3 watched metric — see ESP32-S3-N16R8: 512 KB SRAM + 8 MB PSRAM. We budget conservatively. -| Region | Approx | Use | -|---|---|---| -| Internal SRAM | ~120 KB | task stacks, DMA buffers, hot code paths | -| Internal SRAM | ~80 KB | mbedtls runtime (TLS handshake working set) | -| PSRAM | ~128 KB | EPD framebuffer (792×272×1 ≈ 27 KB) + shadow + glyph cache | -| PSRAM | ~512 KB | rope buffer + edit history headroom | -| PSRAM | ~1.5 MB | gitoxide working set during push (pack delta, etc.) | -| PSRAM | rest | heap headroom | +| Region | Approx | Use | +| ------------- | ------- | ---------------------------------------------------------- | +| Internal SRAM | ~120 KB | task stacks, DMA buffers, hot code paths | +| Internal SRAM | ~80 KB | mbedtls runtime (TLS handshake working set) | +| PSRAM | ~128 KB | EPD framebuffer (792×272×1 ≈ 27 KB) + shadow + glyph cache | +| PSRAM | ~512 KB | rope buffer + edit history headroom | +| PSRAM | ~1.5 MB | gitoxide working set during push (pack delta, etc.) | +| PSRAM | rest | heap headroom | PSRAM is the default for `Box::new` via a custom allocator wrapper; DMA-able allocations must explicitly request internal SRAM (epd-waveshare needs this @@ -264,13 +264,13 @@ does not implement key rotation. Mirrored as live conflicts in [qfd.md §7 "Conflicts left explicitly unresolved by v0.1"](qfd.md#7-tradeoffs-and-their-why-linked-to-adrs). -| Risk | Symptom we'd see | Fallback | -|---|---|---| -| `gix` smart-HTTP push doesn't work on `esp-idf-rs` mbedtls | spike 7 fails | switch to `libgit2-sys` (C, well-trodden) for v0.1 only | -| TinyUSB host drops HID reports under load | dropped keystrokes during fast typing | enable larger USB rx buffer; if still bad, fall back to BLE-HID for v0.1 | -| EPD partial refresh slower than 200 ms | typing feels laggy | reduce font size to shrink dirty area; or render multi-char bursts | -| TLS heap pressure on PSRAM | OOM during push | tune mbedtls to smaller cipher suites; force GC of glyph cache before push | -| SD + EPD on same SPI bus collide | corruption on save during render | move SD to a separate SPI peripheral (ESP32-S3 has two) | +| Risk | Symptom we'd see | Fallback | +| ---------------------------------------------------------- | ------------------------------------- | -------------------------------------------------------------------------- | +| `gix` smart-HTTP push doesn't work on `esp-idf-rs` mbedtls | spike 7 fails | switch to `libgit2-sys` (C, well-trodden) for v0.1 only | +| TinyUSB host drops HID reports under load | dropped keystrokes during fast typing | enable larger USB rx buffer; if still bad, fall back to BLE-HID for v0.1 | +| EPD partial refresh slower than 200 ms | typing feels laggy | reduce font size to shrink dirty area; or render multi-char bursts | +| TLS heap pressure on PSRAM | OOM during push | tune mbedtls to smaller cipher suites; force GC of glyph cache before push | +| SD + EPD on same SPI bus collide | corruption on save during render | move SD to a separate SPI peripheral (ESP32-S3 has two) | Every one of these is detected by a spike before integration starts — we are not finding them at the end.