Compare commits

...

2 Commits

Author SHA1 Message Date
Julien Calixte
f4af38d8ef docs: add .typoena.toml config and auto-sync energy tradeoff
Record the git-tracked .typoena.toml preferences file (save_on_idle,
auto_sync) and the palette `>` command mode that edits it in the v0.5
roadmap. Add a tradeoff-curves note deriving Wi-Fi sync energy as a 1/T
curve, which sets auto_sync's default to 10m (opportunistic, min-clamped)
rather than a 5m wall-clock timer.
2026-07-11 14:38:07 +02:00
Julien Calixte
14967a06c6 docs: retire resolved SD risk, re-sync v0.1 status
Spike 3 (SD) is resolved (genuine 32 GB card mounts, verified on its own
SPI3 host per ADR-012) and the mount/save path is now wired into main.rs.

- README: move the SD/CMD59 open question to the retired-risks line; drop
  the stale "blocked on SD (waiting on a compatible card)" from the status
  blurb. Remaining v0.1 gate is the boot splash + wiring git publish.
- roadmap: flip the boot-load and save-to-SD v0.1 items to done, re-sync
  the status blurb and macroplan note, re-point the config-at-boot TODO
  from the (landed) SD wiring to the git-publish wiring.
2026-07-11 14:14:08 +02:00
5 changed files with 197 additions and 32 deletions

View File

@@ -5,11 +5,12 @@ mechanical keyboard. You write Markdown, you commit, you push. Nothing else
runs on it.
> **Status: pre-MVP, hardware on bench.** Display, USB keyboard, live typing
> with partial refresh, Wi-Fi + TLS, and on-device git push are all verified
> in spikes. No release has shipped yet — v0.1 is blocked on SD (waiting on a
> compatible card), the boot splash, and wiring the save/publish path into the
> app binary. Live per-item status: [`docs/roadmap.md`](docs/roadmap.md) ·
> failure write-ups: [`docs/postmortems/`](docs/postmortems/README.md).
> with partial refresh, Wi-Fi + TLS, SD storage, and on-device git push are all
> verified in spikes; SD mount and save are now wired into the app binary. No
> release has shipped yet — v0.1's remaining gate is the boot splash and wiring
> git publish (`Ctrl-G` → push) into the app binary. Live per-item status:
> [`docs/roadmap.md`](docs/roadmap.md) · failure write-ups:
> [`docs/postmortems/`](docs/postmortems/README.md).
---
@@ -142,8 +143,6 @@ package.json pnpm + oxfmt — formatting toolchain for docs/JSON
## Open questions / risks (tracked, not yet resolved)
- [ ] SD on the bench: the 133 GB card rejects CMD59; waiting on a genuine
≤32 GB card ([postmortem](docs/postmortems/2026-07-05-spike3-sd-cmd59.md)).
- [ ] Heap fragmentation over a long writing session with the PSRAM allocator.
- [ ] Real-world e-ink ghosting with the current partial-refresh cadence.
- [~] Use-after-free freeing the in-flight USB transfer on keyboard unplug —
@@ -151,7 +150,8 @@ package.json pnpm + oxfmt — formatting toolchain for docs/JSON
([`MEMORY_AUDIT.md`](MEMORY_AUDIT.md) finding #1).
Retired risks ([gix push](docs/postmortems/2026-07-05-spike7-gix-https-push.md),
TinyUSB HID stability, TLS heap, libgit2-on-xtensa) and how they died:
[SD CMD59 rejection](docs/postmortems/2026-07-05-spike3-sd-cmd59.md), TinyUSB HID
stability, TLS heap, libgit2-on-xtensa) and how they died:
[`docs/spikes.md`](docs/spikes.md) and
[`docs/postmortems/`](docs/postmortems/README.md).

View File

@@ -32,3 +32,4 @@
| [`spikes.md`](spikes.md) | Rendering & UX spikes — display/UX risks proved outside the hardware stack. |
| [`postmortems/`](postmortems/README.md) | Bring-up debugging write-ups: what broke, the root cause, and the decisions that came out of it. |
| [`notes/`](notes/README.md) | Longer-form essays on the thinking behind specific choices. |
| [`tradeoff-curves/`](tradeoff-curves/README.md) | Cost-vs-knob curves behind chosen defaults — energy, latency, memory. |

View File

@@ -10,11 +10,13 @@ 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: v0.1's hardware gate (SD, splash, wiring
the git/save path into the app binary) is still open, even though 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. 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.
Marks: `[x]` done in core · `[~]` partially done · `[ ]` not started. An
inline `(✓)` marks the done half of a split item.
@@ -33,7 +35,7 @@ name = "v0.1 it writes, it pushes"
start = 2026-06-01
original = 2026-06-29
status = "at-risk"
note = "Overdue — core editing + on-device git push proven in spikes, but blocked on SD (compatible ≤32 GB card), boot splash, and wiring save/publish into main.rs."
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."
[[feature]]
name = "v0.2 navigation"
@@ -67,6 +69,7 @@ note = ": command-line mechanism and :fmt done early; Visual mode not started."
name = "v0.5 palette + multi-file"
start = 2026-09-07
original = 2026-09-28
note = "Also adds the git-tracked .typoena.toml preferences file (save_on_idle, auto_sync cadence) and the palette `>` command mode that edits it live."
[[feature]]
name = "v0.6 markdown"
@@ -108,31 +111,43 @@ requires = ["v0.1 it writes, it pushes"]
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. **Blocked** on three
integration items: SD (Spike 3 — awaiting a compatible ≤32 GB card), the boot
splash (Spike 9), and wiring the git/save path into the app binary — today it
lives in the `git_sync` / `sd_fat` spike bins, not `main.rs`.
**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).
- [~] ESP32-S3 boots (✓); e-ink shows Typoena splash + boot log — splash pending Spike 9
- [x] USB host enumerates the Nuphy, key events reach the editor (Spike 4)
- [ ] One hard-coded file (`/sd/repo/notes.md`) opens on boot — SD spike-only,
not in `main.rs`. The card is pre-seeded from a computer (`just init`
copies a full clone to `/sd/repo` + writes config), never cold-cloned on
device — see [note](notes/git-sync-images-and-repo-size.md).
- [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
card / repo / unreadable note halts with a panel message). The card is
pre-seeded from a computer (`just init` copies a full clone to `/sd/repo` +
writes config), never cold-cloned on device — see
[note](notes/git-sync-images-and-repo-size.md).
- [x] Insert-only editing, backspace, enter, arrow keys — modal editor overshot this early (see v0.2)
- [x] Line wrap, no line numbers yet — soft-wrap done early (see v0.6)
- [ ] Save on `Ctrl-S` → SD — SD blocked, not wired to `main.rs`
- [x] Save to SD via `:w` (and `:sync`) — **wired in `main.rs`** through the
`persistence` module's atomic write (unlink-then-rename + `*.tmp`
boot-recovery)
- [~] Wi-Fi credentials + remote URL + PAT + author: today baked into the binary
via `env!()` (no NVS, no on-device provisioning UI in v0.1). Migrating to
`/sd/typoena.conf` on the card, provisioned by `just provision` (or
`just init` for a fresh card) from the same `firmware/.env` the build uses
(minimum input — rotate the PAT or switch networks without a reflash, no
card re-copy). Firmware to read it at boot instead of
`env!()`TODO, rides with the SD wiring into `main.rs`.
- [~] `Ctrl-G` runs: `git add .` → commit with an ISO-8601 timestamp message →
`git push`; on push failure, `git pull --no-edit` then retry the push
(no-op short-circuit when nothing is staged). Proven on device in the
`git_sync` spike (✓); not yet wired to the editor.
`env!()`the git-publish wiring landed with baked config (2026-07-11);
the `typoena.conf` migration itself is deferred to v0.9 (on-device
provisioning).
- [x] Publish on **`:sync`** (the editor's command; the roadmap originally
planned `Ctrl-G`): stage `notes.md` → commit with a timestamp message →
fast-forward `push`; on a rejected push, fetch + reconcile then retry once
(no-op short-circuit when the tree is unchanged). **Wired into the editor and
hardware-verified 2026-07-11** — `firmware::git_sync` opens the SD `/sd/repo`,
runs on a dedicated 96 KB git thread with lazy Wi-Fi, and pushes over mbedTLS
HTTPS+PAT; the panel snackbar shows `synced <oid>` / `up to date` /
`sync failed`. (Interrupted-push auto-retry deferred to v0.9.)
- [x] Split the display into a **writing column** (60 cols) + a **side panel**
(~30 cols at FONT_6X10) for metadata — the surface every later panel
feature writes to. **Built** in the `editor` crate (`draw_panel`): a
@@ -144,13 +159,15 @@ lives in the `git_sync` / `sd_fat` spike bins, not `main.rs`.
[product § Screen layout](v0.1-mvp-product.md#screen-layout).
- [x] **Snackbar** — a transient side-panel notice for host events (added
2026-07-11). On-device there is no serial log, so boot posts `loaded
<name>` (the note's filename without suffix) and `:w`/`:sync` post `saved`
/ `save FAILED - retry :w`; when git publish is wired it will show the
push result. Set via `Editor::set_notice`; cleared on the next keystroke
<name>` (the note's filename without suffix), `:w` posts `saved` /
`save FAILED - retry :w`, and `:sync` posts `syncing...` then the push
result (`synced <oid>` / `up to date` / `sync failed`). Set via
`Editor::set_notice`; cleared on the next keystroke
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); full refresh on save — save not wired yet
- [~] Partial refresh on edits (✓ Spike 5); save now wired (full-area partial
repaint on `:w`)
Out of scope: Vim, palette, multiple files, branches, conflict handling.
@@ -243,6 +260,36 @@ buffer-lifecycle risk first).
- [ ] The side panel briefly shows file count on `Ctrl-G` when the publish bundles
more than one dirty Tracked file (e.g. `"publishing 3 files: abc1234"`),
so workspace-scoped behaviour stays visible to the user
- [ ] **Preferences file** `/sd/repo/.typoena.toml` — a git-tracked,
hand-editable TOML file for editor behaviour, deliberately **distinct from
the `/sd/typoena.conf` card secrets** (Wi-Fi / PAT / remote / author,
gitignored, never committed — see v0.1). Read at boot; a missing file or
key falls back to the defaults below. Keys:
- [ ] `save_on_idle` (bool, default `true`) — auto-save the current buffer on
the existing idle pause (the ≥ 1 s typing-pause the panel already uses
for its refresh), so `:w` becomes optional rather than required.
- [ ] `auto_sync` (duration string, default `"10m"`; `"0"` / omitted
disables; **min clamp ~`"2m"`** so a palette typo can't drain the
battery) — a *max-staleness cap*, not a wall-clock timer:
**opportunistic, rate-limited** Publish. Push when already awake + dirty
(coalesced into the idle-pause, ≤ once per `auto_sync`) and once on the
way into sleep if dirty; **never wake from deep sleep purely to sync**.
Wi-Fi energy is a `1/T` curve whose knee sits at 510 min, and
`save_on_idle` already owns local data safety — so 10 min halves the
sync energy of a 5-min default for no real risk. Full derivation:
[`tradeoff-curves/wifi-auto-sync.md`](tradeoff-curves/wifi-auto-sync.md).
The **schema + defaults live here in v0.5**; the periodic side rides the
better-git work (v0.7) and must interact with light / deep sleep (v0.8).
- [ ] Open question: because the file is committed, these prefs **sync to
every device** that clones the repo — a per-device sync cadence may
instead want a card-local override (in `typoena.conf`). Decide before
build.
- [ ] **Palette command mode** — typing `>` at the `Ctrl-P` palette switches it
from file search to a command list (VS Code-style). The v0.5 commands edit
the `.typoena.toml` prefs above — e.g. `> save on idle: on/off` and
`> auto sync: 10m` — writing the value back to the file and applying it
live. This command list is the discoverable surface that later actions
(`:fmt`, theme, font) also register into.
## v0.6 — Markdown affordances — [~]

View File

@@ -0,0 +1,12 @@
# Tradeoff curves
> Where a design knob has a cost that bends — energy, latency, memory — against
> an interval or size, the curve and its knee live here, so the chosen default
> is traceable to a shape rather than a guess.
>
> Docs index: [`../README.md`](../README.md). Project overview:
> [`../../README.md`](../../README.md).
| Curve | What it decides |
| --- | --- |
| [`wifi-auto-sync.md`](wifi-auto-sync.md) | `auto_sync` interval vs Wi-Fi energy (a `1/T` hyperbola) — why the default is 10 min and opportunistic, not a wall-clock timer. |

View File

@@ -0,0 +1,105 @@
# Wi-Fi energy vs auto-sync interval
> **Decision:** `auto_sync` defaults to **10 min**, and is an *opportunistic,
> rate-limited* push — not a wall-clock timer that wakes the device. See
> [Policy](#policy). Backs the `.typoena.toml` `auto_sync` key in
> [`../roadmap.md`](../roadmap.md) (v0.5), whose runtime timer lands in v0.7 and
> must respect sleep (v0.8).
>
> Tradeoff-curves index: [`README.md`](README.md). Docs index:
> [`../README.md`](../README.md).
## The model
For a **text** commit the git payload is a few KB — negligible. Almost all the
energy of one sync is a *fixed* radio burst that costs the same no matter how
little changed:
```
radio wake → AP association → TLS handshake → tiny push → teardown
```
So energy per unit time scales as **(fixed cost per sync) × (syncs per hour)**:
```
E(T) = K / T T = interval in minutes, K = one burst's worth of energy
```
A hyperbola. Doubling the frequency doubles the cost; the words you actually
wrote barely move it.
Placeholder constants (pending the v0.8 bench measurement — "measure idle /
typing / push current draw"): an ~8 s radio burst at ~150 mA average ⇒
**0.33 mAh per sync**, so `K ≈ 20 mAh·min/hr`. The vertical scale below moves
with the real measurement; the *shape* and the knee do not.
## The curve
```
Wi-Fi energy vs auto-sync interval E(T) ≈ K / T
mAh/hr
20 | * each sync ≈ one fixed radio burst,
| * independent of how much text changed
| *
15 | * ← STEEP: every extra sync/min costs a full burst
| * for zero payload benefit
| *
| *
10 | *
| *
| *
| * ← knee
5 | *·.___ (5 min)
| `·-·__ ______
| `·-·__·--·______ (15) diminishing returns:
0 | `·--·----·----·----·----·--- the tail is ~flat
+----+----+----+----+----+----+----+----+----+----+----+----+
0 5 10 15 20 25 30 35 40 45 50 55 min
└── knee: 510 min. Left of here you pay a lot;
right of here you save almost nothing.
```
| interval | syncs/hr | Wi-Fi mAh/hr | vs 5-min | per 8 h day |
| ---: | ---: | ---: | ---: | ---: |
| 1 min | 60 | 20.0 | 5.0× | 160 mAh |
| 2 min | 30 | 10.0 | 2.5× | 80 mAh |
| 5 min | 12 | 4.0 | 1.0× | 32 mAh |
| **10 min** | 6 | **2.0** | **0.5×** | 16 mAh |
| 15 min | 4 | 1.33 | 0.33× | 10.7 mAh |
| 30 min | 2 | 0.67 | 0.17× | 5.3 mAh |
| 60 min | 1 | 0.33 | 0.08× | 2.7 mAh |
## Two things that move where "best" sits
**`save_on_idle` already prevents data loss — auto-sync is only remote-mirror
freshness.** The durable local copy is the SD write on the idle pause. A longer
sync interval never risks *losing work*; it only means the GitHub mirror is a
few minutes staler. That's a weak cost, and it pushes the optimum toward
*longer* intervals.
**The real battery risk is the sleep interaction, not the awake case.** While
you're typing, the CPU/e-ink baseline dwarfs the sync cost — 5 vs 15 min is
noise. The damage happens when the device is idle or asleep and a wall-clock
timer wakes it *just to push*: each wake pays the radio burst plus the wake/boot
cost and blocks the low-power state. That turns "closed on the desk overnight"
from weeks of standby into dead-by-morning.
## Policy
Ship `auto_sync` as an opportunistic, rate-limited push, with the config value
read as a *max-staleness cap* rather than a timer period:
- **Push when already awake + dirty**, coalesced into the existing idle-pause,
rate-limited to at most once per `auto_sync` — so a fast typist pausing every
20 s doesn't sync 100×/hr.
- **Push once on the way into sleep** (idle → light sleep, and especially
lid-close → deep sleep) if dirty. This is the highest-value sync: nearly free
(the device is spinning up anyway) and it's the freshness guarantee.
- **Never wake from deep sleep purely to sync.** The one behavior that wrecks
standby life.
On the single number: **10 min** halves the sync energy versus a 5-min default
for essentially no real cost, because `save_on_idle` already owns data safety.
Clamp the minimum to **~2 min** so a palette command (`> auto sync: 10s`) can't
quietly drain the battery.