Commit Graph

174 Commits

Author SHA1 Message Date
Julien Calixte
d63a66f4cf docs(case): document the plan section and embed its render 2026-07-11 08:46:47 +02:00
Julien Calixte
e28a6a6d19 feat(case): add exploded horizontal plan section
New `show="plan"` lifts the deck/screen half off the cavity half so the
board layout reads at a glance — standoffs, corner posts, ports, and the
internal FPC clearance. Tunable via `plan_z` and `explode`.
2026-07-11 08:46:47 +02:00
Julien Calixte
c521d68ad7 docs(case): describe the FPC clearance as internal, refresh renders 2026-07-11 03:04:48 +02:00
Julien Calixte
8fa727702e fix(case): keep the FPC clearance internal, unbroken bezel
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.
2026-07-11 03:04:48 +02:00
Julien Calixte
0c3b5b50b1 docs(case): refresh renders and document the FPC bay + section
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).
2026-07-11 03:01:26 +02:00
Julien Calixte
ff0ff0b5ae feat(case): section view, foam gasket, and left-side FPC bay
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.
2026-07-11 03:01:19 +02:00
Julien Calixte
704fd743a0 docs: link the enclosure concept from README and hardware.md 2026-07-11 02:31:50 +02:00
Julien Calixte
66595c7767 feat(case): add parametric 3D-printed enclosure concept
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.
2026-07-11 02:31:44 +02:00
Julien Calixte
13d49930cc docs: document the SD card provisioning recipes
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.
2026-07-11 01:44:44 +02:00
Julien Calixte
78958d9de6 feat(justfile): split SD provisioning into init/load/provision
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.
2026-07-11 01:44:38 +02:00
Julien Calixte
e92f9c15d3 chore: gitignore Cargo.lock in display and editor crates
Match the workspace convention (firmware, keymap already ignore their
lockfiles); these two library crates just never got a .gitignore when
they were extracted.
2026-07-11 01:25:15 +02:00
Julien Calixte
9ea8e74394 chore(justfile): add seed-sd recipe to pre-seed the notes repo
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.
2026-07-11 01:24:56 +02:00
Julien Calixte
da23b95cfd feat(editor): add :w/:sync commands as host save/publish effects
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.
2026-07-11 01:24:46 +02:00
Julien Calixte
e6498cdcb3 docs(roadmap): mark v0.2.5 + the v0.2 UTF-8 buffer done
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.
2026-07-11 00:54:55 +02:00
Julien Calixte
182526cdc8 feat(keymap): repurpose the physical Esc key to type backtick/tilde
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.
2026-07-11 00:40:34 +02:00
Julien Calixte
bc13c5b065 feat(usb-kbd): compose dead-key accents before enqueuing keys
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.
2026-07-11 00:30:38 +02:00
Julien Calixte
bdabfcf935 fix(editor): make the buffer UTF-8-correct for accented input
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).
2026-07-11 00:29:04 +02:00
Julien Calixte
e8063a2b13 refactor(editor): extract editor + display into host-testable crates
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.
2026-07-11 00:28:37 +02:00
Julien Calixte
875658a661 feat(wifi): retry association with backoff in shared connect helper
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.
2026-07-10 23:44:02 +02:00
Julien Calixte
4460475fb0 feat(keymap): add US-International dead-key accent composer
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.
2026-07-10 23:43:15 +02:00
Julien Calixte
6c284a94db docs: record memory-audit remediation status (#1, #3, decode tests) 2026-07-10 10:36:52 +01:00
Julien Calixte
8edd3badfc fix(usb-kbd): quiesce in-flight transfer before free; guard hot-plug leaks
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.
2026-07-10 10:36:47 +01:00
Julien Calixte
2cd3bba98d feat(keymap): extract pure HID decode into host-testable crate
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.
2026-07-10 10:36:37 +01:00
Julien Calixte
f373892870 docs: add memory-safety audit of the Rust unsafe/FFI surface 2026-07-10 10:22:21 +01:00
Julien Calixte
371b542d44 docs(roadmap): plan Markdown snippets for v0.6
Trigger-driven expansion (no popup — e-ink refresh + distraction-free),
numbered empty tab stops + $0, no placeholder text, no dynamic values,
pause-triggered panel indicator. Records the why so scope isn't relitigated.
2026-07-08 22:44:33 +02:00
Julien Calixte
f197df6dbe style(editor): drop the mode line to the panel's bottom edge
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.
2026-07-08 22:01:10 +02:00
Julien Calixte
ad047cc717 docs: reconcile screen-layout docs with the panel changes
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).
2026-07-08 21:48:32 +02:00
Julien Calixte
ba4a8bfad8 feat(editor): put the mode indicator at the side-panel bottom-left
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.
2026-07-08 21:48:21 +02:00
Julien Calixte
50f650fef6 feat(editor): split display into writing column and side panel
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.
2026-07-08 21:33:41 +02:00
Julien Calixte
ddd9c387d3 feat(usb): track keyboard connection state
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.
2026-07-08 21:33:32 +02:00
Julien Calixte
d6b9e46ec1 docs: add index READMEs for docs/ and docs/notes/
Give the two linked-but-unindexed folders a browsable entry point so
directory links land on a real index instead of a bare listing.
2026-07-08 14:45:58 +02:00
Julien Calixte
dbe7720a27 docs: point directory links at their folder's README
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.
2026-07-08 14:45:52 +02:00
Julien Calixte
fcf5815468 docs(roadmap): track the writing column + side panel split in v0.1
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.
2026-07-08 11:02:41 +02:00
Julien Calixte
aa068147ab docs(roadmap): replace the mermaid Gantt with Macroplan TOML
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.
2026-07-08 00:19:28 +02:00
Julien Calixte
c4326421fd docs(readme): link the gix postmortem where it is mentioned 2026-07-08 00:06:31 +02:00
Julien Calixte
fd6ccb729c Merge remote-tracking branch 'origin/main' 2026-07-08 00:05:48 +02:00
Julien Calixte
3128bca4ad docs(readme): sync to bench reality and slim via links
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.
2026-07-08 00:04:49 +02:00
Julien Calixte
c82130a616 docs(roadmap): move the macro-plan Gantt in from the README
Kept as the original June 2026 baseline with a note pointing to the
Status block for actuals.
2026-07-08 00:04:32 +02:00
Julien Calixte
571c5ed825 docs(hardware): add part table, rationale, and bench status page
Extracted from the README so it can slim down to a summary + link.
2026-07-08 00:04:25 +02:00
b401ea2906 Updating docs/roadmap.md from Remanso 2026-07-07 23:52:38 +02:00
Julien Calixte
77768cc85f docs(roadmap): sync marks to actual core progress
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.
2026-07-07 23:44:46 +02:00
Julien Calixte
b746d6e3f9 feat(editor): add Markdown affordances and a :fmt formatter
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
2026-07-07 23:44:46 +02:00
Julien Calixte
2aa4032e1f docs(notes): record why the notes repo stays full-size (pre-seed the SD)
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.
2026-07-07 22:55:59 +02:00
Julien Calixte
ef755bac81 docs(postmortems): correct the FATFS delete finding (std, not read-only)
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.
2026-07-07 22:49:18 +02:00
Julien Calixte
8dece735d1 fix(firmware): delete repo dir via path-based recursion, not remove_dir_all
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.
2026-07-07 22:48:17 +02:00
Julien Calixte
8e848894fa feat(firmware): add RECLONE_EACH_BOOT toggle to git_sync
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.
2026-07-07 09:33:08 +02:00
Julien Calixte
39e1155bf9 fix(libgit2): create objects writable so FAT can delete them
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).
2026-07-07 09:33:08 +02:00
Julien Calixte
4153cae9c4 docs(postmortems): record milestone #2A verified, FAT read-only-delete finding
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.
2026-07-07 08:35:11 +02:00
Julien Calixte
afa61deaa6 fix(firmware): recover git_sync from a leftover clone dir
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`.
2026-07-07 08:34:09 +02:00
Julien Calixte
859c4787cf feat(firmware): add persistent-clone git sync spike (git_sync)
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).
2026-07-07 00:06:13 +02:00