# Wearable Shell 0.6a

Status: host validated and firmware built; physical AMOLED acceptance is still
required. The firmware has **not** been flashed by this work.

## Baseline and checkout

- Branch: `codex/wearable-shell-0.6a`
- Worktree: `/home/lighthouse/esp32-dev/.cache/wearable-shell-0.6a`
- `stable_base_commit=124feccfe92922c2833735ceded9a726e257ecb1`
- Text + Voice baseline: `0f4f47ccbe79142a1b8cc324c62b856ef6433ff6`
- Network-resilience physical PASS layered on that baseline: `124fecc`
- Research-only spike: `codex/wearable-shell-spike-0.6` at `a6effe7`
- Isolated speech experiment: `codex/speech-buffered-playback-0.5.2` at `a0f020c`; no commits or code were merged from it.
- At the audit, the `main` ref was `3b46835` and the primary checkout was clean on
  `codex/speech-40ms-gateway-ab-0.5.1` at `2fa0dad`. It was not used as the base.
- Production systemd drop-in currently names this 0.6a worktree as the Gateway
  `WorkingDirectory`/`ExecStart`; this work did not restart or mutate the service.

The 0.6 spike was used only as research. The Pillow circular-boundary helper,
deterministic PNG generation, contact-sheet pattern, and offline gallery were
selectively migrated from `607183c` because they are pure, validated host tools.
Their behavior (fixed 468×466 canvas and 217 px safe radius) was preserved; all
launcher/project/session scenes and interaction assumptions were replaced with
the implemented 0.6a page ring.

## Product shell

The implemented normal-page sequence is explicit and bounded:

`Watch ↔ Agent ↔ Monitor/{Overview, CPU, Memory, Disk, Network} ↔ Help`

Short A advances and Short B reverses immediately. There is no launcher, click
window, delayed single-click dispatch, or double-click gesture. Home Screen is
only the persisted boot destination (Watch by product default; Agent remains an
available demo choice). Long A keeps the existing Voice path; Long B opens
Settings. Approval and Voice confirmation retain modal priority and their
existing A/B meanings.

- Watch: battery voltage, Wi-Fi and Gateway state are grouped at the top;
  native 88 px time and 28 px date occupy the visual center, followed by
  IDLE/RUNNING/DONE/FAILED/OFFLINE status and running elapsed time. Server
  metrics, model/effort, firmware and button instructions are absent.
- Agent: compact identity/status/session/model metadata; the body owns 250
  vertical pixels. Normal now shows up to 9 lines, Compact 13 and Large 8
  inside the round-safe region. Running activity tails, tool activity, and terminal
  authoritative `final_text` keep the previous state-selection semantics.
- Monitor: all five original metric pages remain. Typography, safe spacing and
  neutral product hierarchy changed; no charts/history/process browser were added.
- Help: product positioning plus actual A/B, Hold A/B, Approval and Voice keys.
- Settings: Agent (Model/Effort, existing Speech Mode), Device (Brightness,
  Vibration), Display (Home, Text Size, Theme, Agent Clock), System (voltage,
  Wi-Fi, Gateway, firmware).

## Typography audit and implementation

Audited dependency: M5GFX 0.2.21, locked commit
`5268353cffabd2925b87ed9bcc05ded4e22cc3d1`.

- Default M5GFX font is `fonts::Font0`, the fixed 6×8 GLCD bitmap.
- Existing `efontCN_16`/`efontTW_16` are compressed monochrome U8g2 resources,
  not vector fonts or smooth fonts.
- `setTextSize` writes `TextStyle::size_x/size_y`; bitmap renderers multiply run
  coordinates. It is geometric scaling, not interpolation or anti-aliasing.
- M5GFX supports GFX, U8g2, BDF, BFF, runtime VLW and embedded LVGL fonts.
  Runtime raw TTF/TTC construction is conditional on a TTF header that is not
  present in this dependency checkout. No FontConvert, `lv_font_conv`, VLW
  converter, or OTF-to-BDF tool was installed locally.
- Bundled LVGL Montserrat resources are native sizes and use antialiased LVGL
  glyph data. M5GFX expands one LVGL glyph into a temporary aligned vector while
  drawing. Generated CJK and WatchTime88 are native-size 1-bit resources; they avoid
  integer enlargement but are not grayscale antialiased.

Semantic roles are `WATCH_TIME`, `WATCH_DATE`, `PAGE_TITLE`, `STATUS_PRIMARY`,
`STATUS_SECONDARY`, `AGENT_BODY`, `AGENT_BODY_LARGE`, `META`, and `FOOTER`.
Every role renders with scale 1.0. Latin UI uses native LVGL Montserrat
14/16/18/24/28. Watch time uses a native Noto Sans 88 px digits/colon/hyphen
subset. CJK Compact uses the existing
CN16 resource; Normal and Large use generated Noto Sans CJK SC 24/28 subsets.

Fallback is explicit: Montserrat Latin → role CJK SC → `efontCN_16` →
`efontTW_16` → `?`. Missing-glyph counts are included in `[UI_PERF]` logs. Thus a
missing supplementary-plane emoji becomes a replacement glyph without losing
the line, corrupting layout, or crashing.

The reproducible generator uses Pillow FreeType against face 2 of the installed
Noto Sans CJK Regular collection. Required UI/validation characters are ordered
first, followed by GB2312 level 1/2 characters. The original TTF is not copied.
License and source hash are in [font-licenses.md](font-licenses.md).

| Native size | Glyphs | Bitmap bytes | Index bytes | Generated total |
|---:|---:|---:|---:|---:|
| 16 | 3,000 / 3,500 / 5,000 | 96,361 / 112,395 / 160,599 | 48,000 / 56,000 / 80,000 | 144,385 / 168,419 / 240,623 |
| 24 | 3,000 / **3,500** / 5,000 | 211,986 / **247,164** / 353,508 | 48,000 / **56,000** / 80,000 | 260,010 / **303,188** / 433,532 |
| 28 | 3,000 / **3,500** / 5,000 | 287,447 / **335,166** / 479,055 | 48,000 / **56,000** / 80,000 | 335,471 / **391,190** / 559,079 |

Only the bold 24/28 variants are linked. Generated CJK body resources total
694,378 bytes. The 12-glyph WatchTime88 resource adds 4,531 bytes, for 698,909
generated font bytes. The full body-font matrix is
[font-footprints.json](font-footprints.json).

## Preferences, time, and device status

`wearable/prefs_a` and `wearable/prefs_b` use a field-by-field envelope:
magic, version, payload length, generation, payload, CRC32. C++ object layout is
never serialized. Current v3 adds the last Gateway timezone offset; v1/v2 are
migrated, future versions force read-only mode, CRC/length corruption falls back
to the other slot/defaults, and generation saturation prevents ambiguous wrap.

Defaults are brightness 80%, vibration on, Home Watch, Text Normal, Theme Dark,
Agent Clock, with volume reserved and not exposed. Brightness is validated by
the codec, clamped again at the panel boundary, applied at boot, previewed
directly, and saved only on Apply. Vibration
OFF gates generic, DONE, FAILED and Approval motor paths without changing visual
or state behavior.

Gateway `hello_ack` supplies integer UTC milliseconds and timezone offset. A
validated sync updates the monotonic wall clock, writes UTC to RX8130CE, and
persists an offset change. On cold boot, valid RTC BCD/date/weekday and a clear
voltage-low flag seed the clock; network reconnect replaces that truth. Invalid
RTC/time never replaces a valid clock. The implementation follows the official
M5Unified RX8130 register map and enable sequence without copying its driver.

M5PM1 voltage is polled every three seconds outside active recording and shown
as volts. No uncalibrated battery percentage is presented.

## Validation and budget

Mockups: [gallery](wearable-shell-0.6a/artifacts/index.html) and
[contact sheet](wearable-shell-0.6a/artifacts/contact-sheet.png). They cover
Watch, Agent IDLE/RUNNING/DONE Chinese/mixed/tool/long terminal, Approval,
Voice, Monitor, Help and Settings. The host raster uses the same Noto source
for CJK and the 88 px time numerals; it is
not claimed to be pixel-identical to M5GFX.

Host Pillow median / p95 raster timings (100 iterations) are: Watch 3.806 / 5.205
ms; Agent running 12.348 / 14.079 ms; Agent DONE Chinese 11.755 / 11.872 ms;
Help 9.493 / 9.613 ms. These are comparative host values, not physical panel
measurements. Firmware preserves one complete framebuffer scene followed by one
display transaction and logs physical elapsed time plus internal-largest-block,
internal-free, PSRAM-free deltas, and missing glyph count as `[UI_PERF]`.

Relative to base `124fecc`:

| Budget | Base | 0.6a | Delta |
|---|---:|---:|---:|
| app binary | 2,059,840 | 2,915,728 | +855,888 bytes |
| merged flash | 2,125,376 | 2,981,264 | +855,888 bytes |
| linked flash sections | 1,937,092 | 2,792,536 | +855,444 bytes |
| static D/IRAM | 140,255 | 140,919 | +664 bytes |

The 4 MiB app partition has 1,278,576 bytes (30%) free. Static `.data` grew 256
bytes and `.bss` 224 bytes. `DevicePreferences` is 10 bytes,
`LoadedPreferences` 24, `UiState` 48, and the encoded NVS blob 23. Preference
load temporarily uses two 269-byte bounded slot buffers. There is no new
persistent PSRAM allocation; the existing framebuffer and 6,145-byte Agent tail
remain. LVGL Latin rasterization has a per-glyph transient aligned buffer; exact
largest-heap impact is intentionally deferred to the physical `[UI_PERF]` log.

Firmware 0.6.0 artifacts:

- `device_lab.bin`: 2,915,728 bytes
- `merged-flash.bin`: 2,981,264 bytes
- merged SHA-256: `882fcc0fb52098b1f8114988d4f3b347f853c8308455ac2d82964b644ac74fac`
- build command: `./scripts/build.sh m5stack-stopwatch`
- no flash performed

## Explicit exclusions and known limitations

- No A/B double click, double-click window, or single-click delay.
- No Speech output/transport/provider/audio ownership changes. Volume remains reserved.
- No Agent Navigator, Project/Session Picker, Workspace Control, durable draft,
  new Agent type/protocol, touch, IMU gesture, graph/history browser, or network tuning.
- Host font raster and timing do not replace physical AMOLED evaluation.
- Timezone is a fixed offset refreshed on Gateway connection; named timezone/DST
  transitions require a reconnect/resync.
- CJK dynamic coverage is bounded. Less-common glyphs use CN/TW fallback; code
  points beyond all resources show `?`.
- Battery percentage and charging/external-power state remain unreported pending calibration.

## Physical acceptance handoff

1. Watch: verify time/date, 88 px numeral balance, native-edge quality, 80%
   default brightness, voltage and Agent state. Explicitly judge whether the
   face still looks old-fashioned and whether edges are visibly smoother.
2. Navigation: traverse Watch → Agent → five Monitor pages → Help with A, then
   reverse with B. Every short click must react immediately; there is no double click.
3. Help: confirm positioning and all button labels; verify Watch/Agent do not
   carry persistent instructions.
4. Agent: deliver long Simplified, Traditional and mixed CN/EN activity/tool/
   `final_text`; compare Compact/Normal/Large density, line spacing and paging.
5. Voice regression: Long A, record, STT, A Send/B Cancel, then Agent result.
6. Approval regression: confirm modal priority, A Approve and B Reject.
7. Preferences: change brightness, vibration, Home, text size and theme; reboot
   and confirm dual-slot persistence. With vibration off, exercise Approval,
   DONE, FAILED and generic vibration paths.
8. RTC offline: connect once, compare time/offset, disconnect network, confirm
   continued time, reboot offline, then reconnect and confirm resync.
9. Capture `[UI_PERF]` for Watch, Agent running, long Chinese DONE, Help and all
   three text sizes; record elapsed ms, largest internal block and PSRAM deltas.

Recommended next step: flash this artifact only for the above physical gate,
collect screenshots/logs, and adjust font roles/spacing from real AMOLED evidence
before discussing Agent Navigator or any future double-click gesture.
