Compare commits
6 Commits
195311a395
...
54a9e31ee5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54a9e31ee5 | ||
|
|
fc4ce4d017 | ||
|
|
5335751323 | ||
|
|
b899286639 | ||
|
|
698c10c8c0 | ||
|
|
bef9587a34 |
12
docs/adr.md
12
docs/adr.md
@@ -355,6 +355,18 @@ derived key.
|
||||
atomic-rename writes (see
|
||||
[v0.1 technical → `persistence`](v0.1-mvp-technical.md#module-breakdown)
|
||||
and [file layout](v0.1-mvp-technical.md#file-layout)).
|
||||
- **FatFS caveat (Spike 3, verified 2026-07-11):** FatFS's `f_rename` returns
|
||||
`FR_EXIST` on an existing destination — it does **not** replace like POSIX
|
||||
`rename(2)`. So the atomic save must `f_unlink` the target before renaming the
|
||||
`*.tmp` over it, and pair that with **boot recovery**: a lingering `*.tmp` at
|
||||
startup means the last save didn't finish → promote it (it is the newest
|
||||
complete, fsync'd copy). See the
|
||||
[Spike 3 postmortem](postmortems/2026-07-05-spike3-sd-cmd59.md#resolution-2026-07-11).
|
||||
- **SD-card compatibility:** use a genuine card, ideally **≤32 GB (SDHC/FAT32)**.
|
||||
Large or counterfeit SDXC cards may reject `CMD59` (SPI-mode CRC) and fail to
|
||||
mount; we keep CRC required rather than run the user's writing over an
|
||||
unchecked bus. The device reports a swap-the-card message instead of a hex
|
||||
code.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# Spike 3 (SD) — blocked on a card that rejects CMD59 (SPI-mode CRC)
|
||||
|
||||
> Date: 2026-07-05 · Build at time of failure: `07-05 16:07Z @f77f669-dirty`
|
||||
> Status: **paused, not failed** — bench card is incompatible; awaiting a
|
||||
> compliant microSD. Wiring and firmware are proven good.
|
||||
> Status: **RESOLVED 2026-07-11** — a genuine 32 GB SDHC card mounts, negotiates
|
||||
> 10 MHz, and round-trips cleanly on the shared SPI2 bus. Wiring and firmware
|
||||
> were always good; the 133 GB SDXC card's CMD59 rejection was the sole fault.
|
||||
> See [Resolution](#resolution-2026-07-11) below.
|
||||
>
|
||||
> Context: Spike 3 in
|
||||
> [`../v0.1-mvp-technical.md`](../v0.1-mvp-technical.md#hardware-bring-up-order),
|
||||
@@ -10,6 +12,39 @@
|
||||
> firmware notes [`../../firmware/README.md`](../../firmware/README.md).
|
||||
> Spike program: [`../../firmware/src/bin/sd_fat.rs`](../../firmware/src/bin/sd_fat.rs).
|
||||
|
||||
## Resolution (2026-07-11)
|
||||
|
||||
Re-ran `sd_fat` with a genuine 32 GB SDHC card (build `07-11 08:53Z
|
||||
@195311a-dirty`). Everything the paused write predicted held:
|
||||
|
||||
- `sdspi_host: data CRC set=1` — **CMD59 now succeeds**. Init runs clean through
|
||||
card identification (`sdmmc_card_init: card type is SD`).
|
||||
- `card mounted at /sd — max 10000 kHz, negotiated 10000 kHz`.
|
||||
- `FAT usage — 29806 MiB total, 29802 MiB free` (~29.8 GiB of the 32 GB card).
|
||||
- `round-trip OK — 92 bytes: create …tmp → fsync → rename → read back identical`.
|
||||
|
||||
Wiring, pull-ups, bus sharing, and the hand-rolled FFI mount path all confirmed
|
||||
correct — the only variable that changed was the card, exactly as diagnosed. The
|
||||
`cmd=52` / `cmd=5` "command not supported" lines still print and are still
|
||||
normal (SDIO probes a memory card ignores).
|
||||
|
||||
### New finding: FatFS `f_rename` does not overwrite (atomic-save is not POSIX)
|
||||
|
||||
The first run created `/sd/spike3.md`; the **second** run failed at `rename tmp
|
||||
-> final` with `File exists (os error 17)`. Root cause is a FatFS semantics
|
||||
difference, not a fault: **`f_rename` returns `FR_EXIST` when the destination
|
||||
already exists** — unlike POSIX `rename(2)`, which atomically replaces it. So the
|
||||
textbook write-`*.tmp` → rename-over-target idiom from ADR-007 does **not** work
|
||||
as-is on FAT; the destination must be unlinked first.
|
||||
|
||||
That unlink opens a crash window: between `f_unlink(target)` and `f_rename`, the
|
||||
target is momentarily gone while `*.tmp` holds the complete, fsync'd new content.
|
||||
The consequence for the real persistence module is a **boot-recovery** rule: a
|
||||
lingering `*.tmp` at startup means the last save didn't finish → promote it
|
||||
(it's the newest complete copy). The spike now does unlink-then-rename and
|
||||
tolerates a missing target, so re-runs are idempotent; recorded in
|
||||
[ADR-007](../adr.md#adr-007-storage-split--fat-on-sd-for-working-copy-littlefs-on-flash-for-config).
|
||||
|
||||
## Summary
|
||||
|
||||
Built the Spike 3 bench program (`sd_fat`): bring up the SD card over the EPD's
|
||||
@@ -143,16 +178,22 @@ it is explicitly **not recommended** and not applied.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- [ ] Re-run with a genuine ≤32 GB card → expect clean mount + round-trip.
|
||||
- [ ] Strip the debug logging once green: remove `CONFIG_LOG_MAXIMUM_LEVEL_DEBUG`
|
||||
from `sdkconfig.defaults` and the `esp_log_level_set` block in `sd_fat.rs`.
|
||||
- [ ] Write up Spike 3 as verified in
|
||||
[`../../firmware/README.md`](../../firmware/README.md) (wiring, LFN
|
||||
requirement, card-compatibility note), matching the other spikes.
|
||||
- [ ] Record a **recommended-SD-card note** for v0.1 (genuine, ≤32 GB;
|
||||
large/counterfeit SDXC may fail CMD59) — product doc / ADR-007.
|
||||
- [ ] Settle the **shared-bus arbitration** decision (EPD lock vs. SPI3 for SD).
|
||||
- [ ] Enable PSRAM, then build Spike 7 (gitoxide push) for the push leg.
|
||||
- [x] Re-run with a genuine ≤32 GB card → clean mount + round-trip (2026-07-11).
|
||||
- [x] Strip the debug logging once green: removed `CONFIG_LOG_MAXIMUM_LEVEL_DEBUG`
|
||||
from `sdkconfig.defaults` and the `esp_log_level_set` block in `sd_fat.rs`
|
||||
(2026-07-11).
|
||||
- [x] Write up Spike 3 as verified in
|
||||
[`../../firmware/README.md`](../../firmware/README.md) (2026-07-11).
|
||||
- [x] Record a **recommended-SD-card note** for v0.1 (genuine, ≤32 GB;
|
||||
large/counterfeit SDXC may fail CMD59) — in
|
||||
[ADR-007](../adr.md#adr-007-storage-split--fat-on-sd-for-working-copy-littlefs-on-flash-for-config)
|
||||
(2026-07-11).
|
||||
- [x] Enable PSRAM (done, `CONFIG_SPIRAM`) and build Spike 7 (git push) — both
|
||||
complete; see [Spike 7 postmortem](2026-07-05-spike7-gix-https-push.md).
|
||||
- [ ] **Still open:** settle the **shared-bus arbitration** decision (EPD lock
|
||||
vs. SPI3 for SD) before wiring persistence into `main.rs`.
|
||||
- [ ] **Still open:** implement the FatFS atomic-save (unlink-then-rename +
|
||||
`*.tmp` boot-recovery) in the real `persistence` module.
|
||||
|
||||
## Artifacts (this session)
|
||||
|
||||
|
||||
@@ -66,6 +66,30 @@ of it. `sdkconfig.defaults` gains the full certificate bundle and a bigger main
|
||||
task stack for the mbedtls handshake — a one-time esp-idf reconfigure on the
|
||||
next build.
|
||||
|
||||
**Spike 3 — SD card (FAT) on shared SPI2: verified 2026-07-11.** A separate
|
||||
binary — [`src/bin/sd_fat.rs`](src/bin/sd_fat.rs), flashed with `just flash-sd` —
|
||||
brings up the SD card on the EPD's SPI2 bus, mounts FAT at `/sd`, and exercises
|
||||
the persistence module's atomic save (write `*.tmp` → fsync → rename →
|
||||
read-back). Wiring: **SCK 12 · MOSI 11** (shared with the EPD) **· MISO 13**
|
||||
(new; the write-only EPD never used it) **· SD CS 10** (EPD CS is 7).
|
||||
|
||||
Bench result (genuine 32 GB SDHC card): mounts at 10 MHz, `29806 MiB total`,
|
||||
atomic round-trip byte-identical. Two findings baked into the code:
|
||||
|
||||
- **Card compatibility.** A 133 GB SDXC card failed init at `CMD59` (SPI-mode
|
||||
CRC); a genuine ≤32 GB card works. We keep CRC required and reject bad cards
|
||||
with a swap-the-card message rather than run over an unchecked bus. See the
|
||||
[Spike 3 postmortem](../docs/postmortems/2026-07-05-spike3-sd-cmd59.md).
|
||||
- **FatFS rename ≠ POSIX rename.** `f_rename` won't overwrite an existing
|
||||
target (returns `FR_EXIST`), so the atomic save unlinks the destination first;
|
||||
the real persistence module must add `*.tmp` boot-recovery. Long filenames
|
||||
(`CONFIG_FATFS_LFN_HEAP`) are required for the two-dot `*.md.tmp` name.
|
||||
|
||||
Still open before persistence lands in `main.rs`: the **shared-bus arbitration**
|
||||
question — the EPD driver holds an exclusive SPI2 lock for its lifetime, so the
|
||||
EPD and an arbitrated SD device can't both be live on one host yet (release/
|
||||
re-acquire around EPD ops, or give the SD its own SPI3). This spike ran SD-only.
|
||||
|
||||
**Spike 5 — partial refresh + typing: verified 2026-07-04.** `main.rs` wires
|
||||
the keyboard to the panel: [`src/usb_kbd.rs`](src/usb_kbd.rs) feeds decoded
|
||||
key-downs (US layout, edge-detected) into a queue, and the main loop keeps a
|
||||
@@ -118,8 +142,8 @@ reseat the jumpers (CS first) before debugging code.
|
||||
|
||||
Next up per
|
||||
[`docs/v0.1-mvp-technical.md`](../docs/v0.1-mvp-technical.md#hardware-bring-up-order):
|
||||
Wi-Fi/TLS (Spike 6, implemented above), then gitoxide push (Spike 7); SD is
|
||||
deferred.
|
||||
Wi-Fi/TLS (Spike 6, implemented above), then git push (Spike 7), then SD
|
||||
(Spike 3) — all verified.
|
||||
|
||||
**Spike 1 — Blink: verified 2026-07-04.** GPIO 2 + on-board WS2812 toggled
|
||||
at 1 Hz with `blink N` on USB-serial, proving toolchain, esp-idf link, and
|
||||
@@ -166,6 +190,58 @@ over USB you should see:
|
||||
at 1 Hz on the serial monitor, and — if an LED is wired from GPIO 2 → 330 Ω
|
||||
→ GND — the LED blinks in lockstep.
|
||||
|
||||
## Provisioning an SD card
|
||||
|
||||
Typoena reads its config and its notes repo from the SD card — it never
|
||||
cold-clones the ~566 MB repo over Wi-Fi + mbedTLS (the
|
||||
[git-sync sizing decision](../docs/notes/git-sync-images-and-repo-size.md)).
|
||||
Instead a Mac prepares the card over a reader, and the device only ever takes
|
||||
the `open` + fast-forward path. The [`justfile`](justfile) has three entry
|
||||
points, each ejecting the card when done:
|
||||
|
||||
```sh
|
||||
just init ~/code/notes # full prep of a fresh card: notes repo + config
|
||||
just load ~/code/notes # (re)copy just the notes repo → /sd/repo
|
||||
just provision # (re)write just the config (rotate PAT, switch Wi-Fi)
|
||||
```
|
||||
|
||||
`init` is the once-per-card command; `load` and `provision` each refresh one
|
||||
half without touching the other. Add a `/Volumes/<name>` as the last argument if
|
||||
more than one removable card is mounted — auto-detect refuses on ambiguity,
|
||||
since a wrong guess would let `rsync --delete` wipe the wrong disk's `repo/`.
|
||||
|
||||
### Config with little to type
|
||||
|
||||
`typoena.conf` (Wi-Fi + PAT + git identity) needs **no `.env`**. Each value runs
|
||||
a ladder — `.env` if present, else derived from tools already on the machine,
|
||||
else an interactive prompt with the derived value as the default:
|
||||
|
||||
| Value | Derived from |
|
||||
| --- | --- |
|
||||
| `TW_REMOTE_URL` | the source repo's `origin` (or the card's existing clone) |
|
||||
| `TW_AUTHOR_NAME` / `TW_AUTHOR_EMAIL` | `git config user.name` / `user.email` |
|
||||
| `TW_GH_USER` | `gh api user` |
|
||||
| `TW_WIFI_SSID` | the Mac's active Wi-Fi network |
|
||||
| `TW_WIFI_PASS` | the System keychain for that SSID (else prompt) |
|
||||
| `TW_PAT` | **never derived** — always typed by hand |
|
||||
|
||||
So a first run is usually: `just init ~/code/notes`, press Enter through the
|
||||
auto-filled defaults, approve the macOS Keychain dialog for the Wi-Fi password
|
||||
(or type it), and paste a fine-grained PAT once. Reading a saved Wi-Fi password
|
||||
triggers a macOS authorization dialog (login password / Touch ID → Allow) —
|
||||
that's macOS guarding a System-keychain secret, not something the recipe can
|
||||
suppress. Keeping [`.env`](.env.example) populated stays a valid override and
|
||||
skips all prompts.
|
||||
|
||||
### Secrets on the card
|
||||
|
||||
FAT has no file permissions, so **physical custody of the card is the only
|
||||
control** over the plaintext `TW_PAT`. Scope it to a fine-grained token with
|
||||
`contents:write` on just the notes repo, so a lost card is a one-token revoke.
|
||||
The PAT is never derived from `gh auth token` (a broad token on removable media
|
||||
would defeat the point) and never echoed — the recipes report each value only as
|
||||
`set` / `MISSING`.
|
||||
|
||||
## Pin choice
|
||||
|
||||
GPIO 2 is a safe general-purpose pin on the ESP32-S3-DevKitC-1: it's not
|
||||
|
||||
@@ -123,7 +123,14 @@ ports:
|
||||
#
|
||||
# just init ~/code/notes # full prep of a fresh card: repo + config
|
||||
# just load ~/code/notes # (re)copy just the notes repo → /sd/repo
|
||||
# just provision # (re)write just the config from firmware/.env
|
||||
# just provision # (re)write just the config (derive + prompt)
|
||||
#
|
||||
# Config (Wi-Fi + PAT + git identity) needs no firmware/.env: each value is
|
||||
# resolved from firmware/.env if present, else derived from tools the machine
|
||||
# already has (git config, gh, the active Wi-Fi network + its Keychain
|
||||
# password), else asked for at an interactive prompt with the derived value as
|
||||
# the default. The PAT is always typed by hand — never derived (a broad
|
||||
# `gh auth token` on a plaintext card would defeat the scoped-token model).
|
||||
#
|
||||
# Add a /Volumes/<name> as the last arg if more than one card is mounted.
|
||||
# The `_`-prefixed recipes are shared internals (hidden from `just --list`).
|
||||
@@ -136,7 +143,7 @@ init repo_src sd_volume="":
|
||||
set -euo pipefail
|
||||
vol="$(just _card "{{sd_volume}}" | tail -n1)"
|
||||
just _load-repo "{{repo_src}}" "$vol"
|
||||
just _write-conf "$vol"
|
||||
just _write-conf "$vol" "{{repo_src}}"
|
||||
just _eject "$vol"
|
||||
|
||||
# (Re)copy just the notes repo to /sd/repo (full clone, gitignored paths
|
||||
@@ -148,8 +155,10 @@ load repo_src sd_volume="":
|
||||
just _load-repo "{{repo_src}}" "$vol"
|
||||
just _eject "$vol"
|
||||
|
||||
# (Re)write just the config (Wi-Fi + PAT + git identity) from firmware/.env,
|
||||
# then eject — e.g. to rotate the PAT or switch networks without touching repo/.
|
||||
# (Re)write just the config (Wi-Fi + PAT + git identity), then eject — e.g. to
|
||||
# rotate the PAT or switch networks without touching repo/. Values are derived +
|
||||
# prompted (see the section header); firmware/.env is an optional override. With
|
||||
# no repo arg, the git remote is derived from the card's existing clone.
|
||||
provision sd_volume="":
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
@@ -237,38 +246,106 @@ _load-repo repo_src vol:
|
||||
# -P for progress on the ~700 MB copy. Scoped to repo/, never the card root.
|
||||
rsync -rtP --delete --modify-window=1 --exclude-from="$ignore_list" "$src/" "$dest/"
|
||||
|
||||
# Write <vol>/typoena.conf (Wi-Fi + PAT + git identity) from firmware/.env
|
||||
# (already loaded via `set dotenv-load`), the same file the build uses — so no
|
||||
# re-typing creds, no prompts, no network. The PAT is written to the file but
|
||||
# only ever reported as set/MISSING, never echoed. FAT has no file permissions,
|
||||
# so physical custody of the card is the control: use a fine-grained PAT
|
||||
# (contents:write on just the notes repo) so a lost card is a one-token revoke.
|
||||
_write-conf vol:
|
||||
# Resolve + write <vol>/typoena.conf (Wi-Fi + PAT + git identity). Each value
|
||||
# runs a ladder: firmware/.env (loaded via `set dotenv-load`) → derived from
|
||||
# tools already on the machine (git config, gh, the active Wi-Fi network + its
|
||||
# System-keychain password) → interactive prompt, with the derived value as the
|
||||
# default. So a newcomer needs no .env: they Enter through the defaults and paste
|
||||
# a PAT once. The PAT is never derived (a broad `gh auth token` on plaintext
|
||||
# removable media would defeat the scoped-token model) and never echoed. FAT has
|
||||
# no file permissions, so physical custody of the card is the control: use a
|
||||
# fine-grained PAT (contents:write on just the notes repo) so a lost card is a
|
||||
# one-token revoke. <repo_src> (optional) seeds the git remote; without it the
|
||||
# remote is derived from the card's existing clone.
|
||||
_write-conf vol repo_src="":
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
conf="{{vol}}/typoena.conf"
|
||||
if [ -n "${TW_WIFI_SSID:-}${TW_REMOTE_URL:-}${TW_PAT:-}" ]; then
|
||||
{
|
||||
printf '# Typoena runtime config — generated by `just provision` from firmware/.env.\n'
|
||||
printf '# Plaintext secrets on removable media: keep the card safe; scope TW_PAT\n'
|
||||
printf '# to contents:write on just the notes repo. `key=value`, `#` = comment.\n'
|
||||
printf 'TW_WIFI_SSID=%s\n' "${TW_WIFI_SSID:-}"
|
||||
printf 'TW_WIFI_PASS=%s\n' "${TW_WIFI_PASS:-}"
|
||||
printf 'TW_REMOTE_URL=%s\n' "${TW_REMOTE_URL:-}"
|
||||
printf 'TW_GH_USER=%s\n' "${TW_GH_USER:-}"
|
||||
printf 'TW_PAT=%s\n' "${TW_PAT:-}"
|
||||
printf 'TW_AUTHOR_NAME=%s\n' "${TW_AUTHOR_NAME:-}"
|
||||
printf 'TW_AUTHOR_EMAIL=%s\n' "${TW_AUTHOR_EMAIL:-}"
|
||||
} > "$conf"
|
||||
mask() { if [ -n "${1:-}" ]; then printf 'set'; else printf 'MISSING'; fi; }
|
||||
echo "wrote $conf (values reused from firmware/.env, never printed):"
|
||||
echo " wifi: ssid=$(mask "${TW_WIFI_SSID:-}") pass=$(mask "${TW_WIFI_PASS:-}")"
|
||||
echo " git: remote=$(mask "${TW_REMOTE_URL:-}") gh_user=$(mask "${TW_GH_USER:-}") pat=$(mask "${TW_PAT:-}")"
|
||||
echo " author: name=$(mask "${TW_AUTHOR_NAME:-}") email=$(mask "${TW_AUTHOR_EMAIL:-}")"
|
||||
else
|
||||
echo "note: no TW_* values in firmware/.env — leaving any existing $conf untouched"
|
||||
echo " (copy .env.example → firmware/.env and fill in Wi-Fi + PAT to provision)"
|
||||
repo_src="{{repo_src}}"; repo_src="${repo_src%/}"
|
||||
interactive=1; [ -t 0 ] || interactive=0
|
||||
|
||||
# ask "label" "default" → chosen value on stdout; prompt shown on stderr (so
|
||||
# it survives the $(...) capture). Non-interactive → returns the default.
|
||||
ask() {
|
||||
local label="$1" def="${2:-}" ans
|
||||
if [ "$interactive" = 0 ]; then printf '%s' "$def"; return; fi
|
||||
read -r -p " $label${def:+ [$def]}: " ans || ans=""
|
||||
printf '%s' "${ans:-$def}"
|
||||
}
|
||||
# like ask but hides input (secrets). A present default reads as "keep current".
|
||||
ask_secret() {
|
||||
local label="$1" def="${2:-}" ans
|
||||
if [ "$interactive" = 0 ]; then printf '%s' "$def"; return; fi
|
||||
read -r -s -p " $label${def:+ [keep current]}: " ans || ans=""; printf '\n' >&2
|
||||
printf '%s' "${ans:-$def}"
|
||||
}
|
||||
|
||||
# ── derive defaults (silent; empty when a tool is missing/unauthed) ──────────
|
||||
# remote: .env → source repo's origin → the card's existing clone.
|
||||
remote="${TW_REMOTE_URL:-}"
|
||||
if [ -z "$remote" ] && [ -n "$repo_src" ] && [ -d "$repo_src/.git" ]; then
|
||||
remote="$(git -C "$repo_src" remote get-url origin 2>/dev/null || true)"
|
||||
fi
|
||||
if [ -z "$remote" ] && [ -d "{{vol}}/{{sd_repo_dir}}/.git" ]; then
|
||||
remote="$(git -C "{{vol}}/{{sd_repo_dir}}" remote get-url origin 2>/dev/null || true)"
|
||||
fi
|
||||
a_name="${TW_AUTHOR_NAME:-$(git config user.name 2>/dev/null || true)}"
|
||||
a_email="${TW_AUTHOR_EMAIL:-$(git config user.email 2>/dev/null || true)}"
|
||||
gh_user="${TW_GH_USER:-}"
|
||||
[ -z "$gh_user" ] && gh_user="$(gh api user --jq .login 2>/dev/null || true)"
|
||||
# ssid: .env → the Mac's active Wi-Fi network.
|
||||
wifi_if="$(networksetup -listallhardwareports 2>/dev/null | awk '/Wi-Fi/{getline; print $2; exit}')"
|
||||
ssid="${TW_WIFI_SSID:-}"
|
||||
[ -z "$ssid" ] && ssid="$(networksetup -getairportnetwork "${wifi_if:-en0}" 2>/dev/null | sed -n 's/^Current Wi-Fi Network: //p')"
|
||||
wifi_pass="${TW_WIFI_PASS:-}"
|
||||
pat="${TW_PAT:-}"
|
||||
|
||||
# ── confirm / fill via prompts ──────────────────────────────────────────────
|
||||
[ "$interactive" = 1 ] && echo "configuring $conf — press Enter to accept each [default]:" >&2
|
||||
ssid="$(ask "Wi-Fi SSID" "$ssid")"
|
||||
# Keychain read happens after the SSID is final (the user may have edited it).
|
||||
# Reading a System-keychain Wi-Fi password can pop a macOS auth dialog — only
|
||||
# attempt it interactively so scripted runs never trigger a surprise prompt.
|
||||
if [ -z "$wifi_pass" ] && [ -n "$ssid" ] && [ "$interactive" = 1 ]; then
|
||||
echo " looking up Wi-Fi password for '$ssid' in Keychain (approve the macOS dialog if it appears)…" >&2
|
||||
wifi_pass="$(security find-generic-password -wa "$ssid" 2>/dev/null || true)"
|
||||
fi
|
||||
wifi_pass="$(ask_secret "Wi-Fi password" "$wifi_pass")"
|
||||
remote="$(ask "Git remote URL" "$remote")"
|
||||
gh_user="$(ask "GitHub username" "$gh_user")"
|
||||
pat="$(ask_secret "GitHub PAT (fine-grained, contents:write)" "$pat")"
|
||||
a_name="$(ask "Commit author name" "$a_name")"
|
||||
a_email="$(ask "Commit author email" "$a_email")"
|
||||
|
||||
# ── require the essentials (a blank config ships a dead device) ──────────────
|
||||
missing=""
|
||||
[ -n "$ssid" ] || missing="$missing Wi-Fi-SSID"
|
||||
[ -n "$remote" ] || missing="$missing git-remote-URL"
|
||||
[ -n "$pat" ] || missing="$missing GitHub-PAT"
|
||||
if [ -n "$missing" ]; then
|
||||
echo "error: missing required config:$missing" >&2
|
||||
[ "$interactive" = 0 ] && echo " (no TTY — set these in firmware/.env or run interactively)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── write ───────────────────────────────────────────────────────────────────
|
||||
{
|
||||
printf '# Typoena runtime config — generated by `just init`/`provision`.\n'
|
||||
printf '# Plaintext secrets on removable media: keep the card safe; scope TW_PAT\n'
|
||||
printf '# to contents:write on just the notes repo. `key=value`, `#` = comment.\n'
|
||||
printf 'TW_WIFI_SSID=%s\n' "$ssid"
|
||||
printf 'TW_WIFI_PASS=%s\n' "$wifi_pass"
|
||||
printf 'TW_REMOTE_URL=%s\n' "$remote"
|
||||
printf 'TW_GH_USER=%s\n' "$gh_user"
|
||||
printf 'TW_PAT=%s\n' "$pat"
|
||||
printf 'TW_AUTHOR_NAME=%s\n' "$a_name"
|
||||
printf 'TW_AUTHOR_EMAIL=%s\n' "$a_email"
|
||||
} > "$conf"
|
||||
mask() { if [ -n "${1:-}" ]; then printf 'set'; else printf 'MISSING'; fi; }
|
||||
echo "wrote $conf (secrets never printed):"
|
||||
echo " wifi: ssid=$(mask "$ssid") pass=$(mask "$wifi_pass")"
|
||||
echo " git: remote=$(mask "$remote") gh_user=$(mask "$gh_user") pat=$(mask "$pat")"
|
||||
echo " author: name=$(mask "$a_name") email=$(mask "$a_email")"
|
||||
|
||||
# Flush + eject so the card can go straight into Typoena.
|
||||
_eject vol:
|
||||
|
||||
@@ -26,12 +26,6 @@ CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=4096
|
||||
# `std::thread::Builder::new().stack_size(XXX)` for spawning
|
||||
CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=4096
|
||||
|
||||
# Raise the compile-time log ceiling so the sdmmc/sdspi drivers' per-command
|
||||
# DEBUG logs (the raw R1 response bytes) are built in. Runtime level stays INFO
|
||||
# by default; the SD spike bumps just the sdmmc/sdspi tags to DEBUG so we can see
|
||||
# exactly what each init command returns. (Diagnostic for Spike 3 init failures.)
|
||||
CONFIG_LOG_MAXIMUM_LEVEL_DEBUG=y
|
||||
|
||||
# FatFS long filenames (Spike 3 — SD). Default is 8.3-only, which rejects the
|
||||
# persistence module's atomic-save temp name (`notes.md.tmp` has two dots).
|
||||
# LFN on the heap keeps the working buffer off the (small) task stack.
|
||||
|
||||
@@ -81,13 +81,6 @@ fn main() -> Result<()> {
|
||||
|
||||
log::info!("Typoena — Spike 3 (SD/FAT on shared SPI2), {BUILD_TAG}");
|
||||
|
||||
// Diagnostic: surface the sdmmc/sdspi drivers' per-command DEBUG logs (the
|
||||
// raw R1 response bytes) so an init failure shows *which* command the card
|
||||
// rejects and with what response — not just the final propagated error.
|
||||
for tag in [c"sdmmc_sd", c"sdmmc_cmd", c"sdmmc_init", c"sdspi_transaction", c"sdspi_host"] {
|
||||
unsafe { sys::esp_log_level_set(tag.as_ptr(), sys::esp_log_level_t_ESP_LOG_DEBUG) };
|
||||
}
|
||||
|
||||
match run() {
|
||||
Ok(()) => {
|
||||
log::info!("✅ Spike 3 complete — mount + atomic write/fsync/rename/read-back on shared bus")
|
||||
@@ -196,11 +189,16 @@ fn mount_sd() -> Result<*mut sys::sdmmc_card_t> {
|
||||
slot.gpio_wp = -1;
|
||||
slot.gpio_int = -1;
|
||||
|
||||
// 4) Mount config. format_if_mount_failed = false is load-bearing: a mount
|
||||
// hiccup must never reformat (and wipe) the user's card. allocation size
|
||||
// only matters when formatting, which we've disabled.
|
||||
// 4) Mount config. format_if_mount_failed = true here (spike only): a fresh
|
||||
// bench card that's exFAT or unformatted gets reformatted to FAT on the
|
||||
// device instead of failing, so no Mac-side prep is needed. This fires
|
||||
// only on a *filesystem* mount failure, not on the earlier CMD59 protocol
|
||||
// rejection (that still bails with the actionable message below).
|
||||
// The real persistence module MUST keep this false — it must never wipe
|
||||
// the user's card on a transient mount hiccup. allocation_unit_size is
|
||||
// used when formatting, so the 16 KiB below now applies.
|
||||
let mount = sys::esp_vfs_fat_mount_config_t {
|
||||
format_if_mount_failed: false,
|
||||
format_if_mount_failed: true,
|
||||
max_files: 4,
|
||||
allocation_unit_size: 16 * 1024,
|
||||
disk_status_check_enable: false,
|
||||
@@ -243,6 +241,19 @@ fn file_roundtrip() -> Result<()> {
|
||||
f.write_all(payload.as_bytes()).context("write tmp")?;
|
||||
f.sync_all().context("fsync tmp")?; // FatFS f_sync — flush before rename
|
||||
}
|
||||
// FatFS's f_rename — unlike POSIX rename(2) — refuses to overwrite an
|
||||
// existing destination and returns FR_EXIST (EEXIST). So the classic
|
||||
// write-tmp → rename-over-target idiom needs an explicit unlink of the
|
||||
// target first on FAT. That opens a crash window: the target is briefly
|
||||
// gone while `tmp` holds the complete, fsync'd new content. The real
|
||||
// persistence module must pair this with boot recovery — a lingering
|
||||
// `*.tmp` means the last save didn't finish and should be promoted. See
|
||||
// ADR-007. (Tolerate a missing target so the first save works too.)
|
||||
match fs::remove_file(&path) {
|
||||
Ok(()) => {}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => return Err(e).context("remove existing target before rename"),
|
||||
}
|
||||
fs::rename(&tmp, &path).context("rename tmp -> final")?;
|
||||
|
||||
let mut back = String::new();
|
||||
|
||||
Reference in New Issue
Block a user