The ribbon accommodation must not be visible from outside. Drop the cut
below the bezel lip so the flex folds back into the cavity with no notch
breaking the external face.
Regenerate all previews for the current model, embed the nameplate and
section views, and correct the screen/FPC captions (ribbon exits the
left, breakout on the left, wiring to the ESP32).
The screen's flex leaves the LEFT short edge (user's left), so route it
there: an open ribbon bay in the left bezel, the DESPI-C579 breakout in
the cavity below it, and SPI wiring across to the ESP32. Add a modeled
foam gasket, a `show="section"` cutaway of the screen clamp, and
`active_off_x/y` for the panel's off-centre active area.
Typewriter-body OpenSCAD model for the GDEY0579T93 panel and
ESP32-S3-DevKitC-1: glueless screen retention (bezel lip + foam gasket
+ screwed bracket), baseplate-as-chassis board mounting, and a recessed
deck nameplate. Ships render previews, a dimensioned concept drawing,
and just recipes to open/render/export.
Record init/load/provision in the roadmap (the config-on-SD migration
and the firmware env!()->file read TODO) and in the git-sync note as the
implementation of the pre-seed-from-a-computer decision.
Replace the single seed-sd recipe with three entry points sharing
private helpers: init (repo + config), load (repo copy only), provision
(config only). Each excludes gitignored paths via git's own resolution
(so node_modules and firmware/.env never reach the card), writes
/sd/typoena.conf from firmware/.env (Wi-Fi + PAT + git identity, PAT
never printed), and ejects the card so it goes straight into Typoena.
Match the workspace convention (firmware, keymap already ignore their
lockfiles); these two library crates just never got a .gitignore when
they were extracted.
The device never cold-clones the 566 MB notes repo over Wi-Fi + mbedTLS;
a laptop copies the clone onto the SD card via a reader so the device
only ever takes the open + fast-forward path. Excludes are driven off
git's own ignore resolution (collapsing node_modules, .env, etc.), and
the target card is auto-detected but refused on 0 or >1 match so rsync
--delete can't wipe the wrong disk.
The pure, no-IO editor core can't persist or push, so `handle()` now
returns an `Effect` (None/Save/Publish) that the firmware actions. `:w`
signals Save, `:sync` signals Publish (save then git push); `:wq`/`:x`
alias Save with vim's "quit" half dropped, and the `:q` family stays
absent — an always-on appliance has nothing to quit to. `:fmt` remains
in-core and yields no effect.
The main loop keeps the batch's last effect and matches on it. Actual
SD-write and git-push are the v0.1 gate and still stubbed as log lines.
International input is hardware-verified (dead-key composer, UTF-8-correct
editor, physical Esc -> backtick/tilde). Records the editor's extraction
into host-testable crates and the deliberately-dropped side-panel accent
marker.
Escape already comes from a Caps tap, so the physical Esc key (HID 0x29)
is redundant. Map it to `/~ instead, which also reaches the grave/tilde
dead-key accents and Markdown code fences without a Fn layer on 60%
boards. Esc is now the Caps tap only.
Route decoded keys through keymap::Composer, so on device `'`+e yields é
(grave/acute/circumflex/diaeresis/tilde, plus ç), and reset it on
keyboard detach. Enabling it is now safe because the editor buffer is
UTF-8-correct. Not verified on the xtensa build in this environment.
Step whole characters everywhere instead of assuming 1 byte = 1 char, so
the dead-key composer's accented Latin-9 output (é, ç, …) no longer traps
the caret mid-character and panics on the next edit. Covers h/l/a and the
Escape step-back, j/k column math, backspace and x, word-end (e) with
de/ce, and layout/caret_rc (byte offsets vs display columns). Adds 7
accented-input tests (15 total).
Move the editor core out of the firmware crate (pinned to the xtensa
target, so it can't run `cargo test`) into a standalone `editor` crate,
with the panel framebuffer + geometry split into a `display` crate. Both
depend only on embedded-graphics + keymap, so the editor is now
host-buildable and unit-tested (8 characterization tests). Firmware links
them and re-exports the geometry from epd.rs; behaviour is unchanged.
The xtensa firmware build was not verified in this environment.
The single-shot connect_wifi aborted boot on any transient association or
DHCP miss — common right after a reset, when the AP still holds the stale
pre-reset association. Add a bounded retry (5 attempts, 500ms→4s backoff,
disconnect between tries) so those transient failures recover instead of
failing the boot.
Extract the logic into firmware::net (new lib target) and rewire the
wifi_tls / git_push / git_sync spikes to it, removing three duplicated
copies. main.rs reuses this when Wi-Fi lands there instead of adding a
fourth copy.
Folds a dead key (' ` ^ " ~) plus the following letter into one Latin-9
Key::Char, with the roadmap's `'`+c -> ç case and `'`+space -> literal
apostrophe. Pure and host-tested (12 new tests, 25 total pass).
Not wired into the live decode path yet: it emits non-ASCII and the
editor buffer still assumes byte==char, so wiring waits on the v0.2
UTF-8-correct buffer.
Adopt the keymap crate (drops the PREV_KEYS/CAPS_USED statics for one
owned Decoder) and harden device teardown:
- #1 UAF: track REPORT_INFLIGHT (set on submit, cleared first thing in
report_cb). Teardown moves to close_device, which pumps client events
until the transfer quiesces before freeing it, and leaks rather than
frees if it never does — a leak is recoverable, a use-after-free is
not. usb_host_transfer_free's return is now checked.
- #3 leaks: a new attach while a device is still open tears the old one
down first; control_request and start_report_polling free the transfer
they allocated on a submit error.
Decode correctness is covered by the keymap tests; the teardown paths
are FFI and still need an on-device hot-plug run to confirm.
The Key type, US-QWERTY translate, and the edge-detecting boot-report
parser lived in usb_kbd.rs, unreachable by cargo test (the firmware
crate is pinned to the xtensa target). Move them to a dependency-free
#![no_std] + #![forbid(unsafe_code)] crate so the one path that parses
untrusted device bytes can be exercised on the host.
14 tests, including an ASCII-invariant sweep over all 256 usage IDs
(pins the guarantee the editor's byte==char indexing relies on) and a
never-panics fuzz over arbitrary-length/content reports.
Move the mode indicator (and the NO KBD flag above it) down so the
mode sits flush at the bottom-left with a 2 px margin, reclaiming the
dead space that was left below it.
Mode now sits at the side panel's bottom-left (not its top), and the
words-this-session field is gone. Update the CONTEXT.md field list and
the product-doc mock + prose to match; note the keyboard flag renders
as NO KBD (Latin-9 has no glyph for the mock's symbols).
Move the mode + pending-command echo from the panel's top to its
bottom-left (vim `-- INSERT --` style); word count takes the top slot
and the NO KBD flag sits just above the mode. Also drop the
words-this-session field (not wanted) and its session_base_words state.
Carve the panel into a ~60-col writing column and a right-hand side
panel (per CONTEXT.md screen regions); the divider sits right of the
x=396 driver seam. The mode indicator and pending-command echo move
into the panel, while the ':' command line stays in the bottom strip.
The panel also shows word count and words-this-session (a snapshot
refreshed on a typing pause / non-Insert action, never per keystroke,
so ordinary typing keeps the fast windowed refresh) and a "NO KBD"
flag while the keyboard is dropped.
Add a persistent KBD_PRESENT atomic (set on setup, cleared on unplug)
and a keyboard_present() accessor. DEV_GONE is a one-shot detach event
the client loop consumes, not a state the panel can read.
Bare-folder links (e.g. docs/postmortems/) only resolve on GitHub's
auto-render; retarget them to the folder's README.md so they open a
real file in any markdown viewer.
The side panel was referenced from v0.2.5/v0.5/v0.8 but never defined or
scheduled. Add it as a v0.1 build item, mark it unbuilt, and link the
CONTEXT.md and product-design definitions.
The macroplan app parses this source directly; the baseline originals
stay fixed and v0.1's slip is expressed as an at-risk status instead of
a re-drawn bar.
Fix stale claims: hardware IS on bench; git is libgit2/git2 as an
esp-idf component (ADR-004 kill-switch fired), not gix; display is the
in-tree SSD1683 driver; TLS handshake heap is the measured ~35 KB; v0.1
auth is build-time TW_* env vars (ADR-011 open). Replace the hardware
table, Gantt, and "why not" sections with links (docs/hardware.md,
docs/roadmap.md, ADR-001/002); update repo layout from planned to
actual; prune resolved open questions and link postmortems.
The editor core runs 2-3 versions ahead of shippable device releases (no
release has shipped; v0.1's hardware gate is still open). Mark done/partial
items across v0.1-v0.7 with [x]/[~], record what landed early (View mode,
d/c operators + text objects, :fmt), and the v0.4 decision to move View off
v/V so Visual can take them.
v0.6 affordances plus the v0.4 command-line mechanism, all in editor.rs:
- soft-wrap display lines at word boundaries (layout() no longer cuts
mid-word; a single word wider than the panel still hard-breaks)
- Enter continues Markdown lists: -/*/+ repeat, N. increments, an empty
item exits the list; only at end-of-line, indentation preserved
- heading lines (# .. ######) render faux-bold via a 1px double-strike
(no bold Latin-9 font); checks the logical line so wrapped headings stay bold
- new `:` command mode (Command variant + status-strip echo, Esc/Backspace
cancel) with one command, `:fmt`: align pipe tables (honoring :--: colons),
collapse runs of blank lines to one, strip trailing whitespace; caret lands
on roughly the same line afterwards
The 150 MB of images are remanso's image CDN (it reads image bytes straight
out of git as blobs), not offloadable bloat — LFS/filter-repo/git-rm all
break remanso. libgit2 also lacks blobless partial clone. Decision: leave
the repo untouched and pre-seed the SD with a full clone from a computer, so
the device only ever takes the open + incremental fetch/push path. Also
notes the increment-C constraint: stage specific paths (not add_all) so a
media-excluding sparse checkout is safe.
The read-only-attribute theory (and the predicted AM_RDO unlink shim) was
wrong. A diagnostic recursive walk showed every entry writable (ro=false)
and succeeded via path-based deletion, so the real blocker was
std::fs::remove_dir_all's openat/unlinkat/fdopendir usage against esp-idf's
path-based FATFS VFS. Update the increment-A/B checklist accordingly.
std::fs::remove_dir_all deletes race-free via the openat/unlinkat/fdopendir
family; esp-idf's path-based FATFS VFS doesn't implement those, so it fails
with EACCES on the first entry. That looked like a read-only-attribute
problem but wasn't. A plain read_dir -> remove_file/remove_dir walk
(remove_tree) uses only what FATFS supports.
Hardware-verified: a diagnostic recursive delete removed the whole
/spiflash/repo (every entry reported writable, ro=false) then re-cloned +
pushed, twice. Wire remove_tree into both the RECLONE_EACH_BOOT reset and
the stale-clone recovery.
The p_open/p_creat shim (39e1155) stays: it keeps libgit2's objects writable
(ro=false), so neither this delete nor libgit2's own gc/repack/fetch-prune
is ever blocked by a read-only file on FAT.
Debug/recovery knob (ships false): when true, wipe /spiflash/repo and re-clone
every boot instead of opening the persistent clone. Doubles as the validation
harness for the FAT read-only-delete fix — the wipe deletes libgit2-created
objects, which only succeeds once they are writable.
libgit2 creates loose objects and packfiles with mode 0444 (read-only). FATFS
turns that into the AM_RDO attribute and then refuses to f_unlink them (EACCES),
and esp-idf's FATFS VFS chmod cannot clear AM_RDO — so a written object can
never be deleted, blocking re-clone recovery and (later) fetch/repack.
Shim p_open/p_creat in esp_stubs.c to force owner-write into every create mode
(compile posix.c's originals under throwaway names, same mechanism as p_rename),
so nothing is ever created read-only. Immutability is only a safety hint on an
appliance where nothing but libgit2 touches these files.
Non-regressive on hardware: git_push, which links the same component, still
commits + pushes cleanly with the shim. The delete path itself is validated via
git_sync's RECLONE_EACH_BOOT knob (pending a dedicated flash).
Increment A (persistent-clone publish cycle) is hardware-verified: clone +
persistent open + fast-forward push. Mark the follow-up checklist: git module
fold is in progress (A done; B = divergence + AM_RDO unlink shim; C = editor
wiring), tech-doc revision done, PAT-in-flash tracked as open ADR-011. Note
the FAT read-only-delete limitation and the real-notes-repo sizing caveat.
859c478 aborted if /spiflash/repo already existed — libgit2 refuses to
clone into a non-empty dir (code=Exists). Remove a writable stale/partial
clone and re-clone.
Milestone #2A now hardware-verified end to end: clone (boot 1) + persistent
open + fast-forward push (boot 2) against origin/main, on the 96 KB git
thread, min heap 6.8 MB — clone/checkout fit the stack, no bump needed.
Known limit: the remove only works on a *writable* leftover. libgit2 marks
objects/packs read-only and FATFS won't f_unlink a read-only file (EACCES);
POSIX chmod does not clear AM_RDO on esp-idf's FATFS VFS (verified). So
re-cloning over a run that wrote objects needs an AM_RDO-clearing unlink
shim in esp_stubs.c (increment B, also needed for fetch/repack); until then
the fallback is a one-time `espflash erase-region 0x310000 0x400000`.
Milestone #2 increment A: the product's real publish flow, distinct from
git_push's fresh-init-per-boot throwaway branch. open-or-clone a persistent
/spiflash/repo, append to a tracked notes.md, commit on top of the branch,
and fast-forward push over mbedTLS HTTPS+PAT. Reconcile handles up-to-date
and fast-forward; a true divergence (merge commit on FATFS) is deferred to
increment B. Runs on the dedicated 96 KB git thread; gated behind `git`.
Compiles clean; on-device clone/open+fast-forward not yet hardware-verified
(clone/checkout is deeper than the proven init path — the flash may need a
GIT_STACK bump).
The PAT-in-flash shortcut (ADR-005 v0.1) is safe for the dev's bench unit
but not for a shipped one: it's plaintext in flash, the same token on every
unit, and rotation needs a reflash. ADR-005 decided the auth model but left
provisioning mechanics open.
Capture that as ADR-011 (Proposed/Open): the options (build-time bake →
on-device paste → eFuse-encrypted NVS, per-device fine-grained PAT, GitHub
App) and the likely shape, deferred beyond v0.1. It gates the first non-dev
distribution. Point the Spike 7 postmortem's shortcut entry at it.
The v0.1 technical doc still described the gitoxide/gix plan with an
undecided transport and a 32 KB git_task stack. Bring it in line with
what spike 7 proved:
- gix → libgit2 via the git2 crate (ADR-004 kill-switch fired: no gix push)
- transport settled: HTTPS + PAT over esp-idf mbedTLS, no custom transport
or reqwest/rustls layer needed
- fail-closed cert verification (embedded GitHub roots + PASSTHROUGH)
- git_task stack 32 KB → 96 KB (measured ~67 KB depth); dedicated thread
- persistent-clone + fast-forward is the product flow (spike used a fresh
per-boot throwaway branch); add --all stages deletions
- PAT via libgit2 credential callback, never logged/persisted
Also mark the Spike 7 entry done in the spikes list.