# Wearable Shell 0.6a

Status: existing Watch Boot Gate **PHYSICAL PASS**, confirmed by the user on
2026-10-05 for source `a4bf8284125e39654d8a88bcc7c5b5c2e4424a07`.
This cleanup preserves that Watch design. Battery, Settings Back, Monitor, fixed
UTC+8 and all existing baselines achieved **PHYSICAL PASS** on source `48d4719`
on 2026-10-05, confirmed by the user for both complete test groups. See
[closeout](hold-a-back-closeout-0.6a.md) and
[acceptance evidence](physical-acceptance-0.6.0.json).

## 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 ↔ 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. Within Settings, Long A returns one level (Edit discards its draft and
restores brightness), Long B closes. LOADING/APPLYING/SAVING blocks Back until
completion; Model/Effort and Speech have explicit parent transitions. Approval and Voice confirmation retain modal priority and their
existing A/B meanings.

- Watch: estimated battery percentage, 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: one Overview shows CPU, MEM, DISK and one-line NET RX/TX at the same
  native body role. Width is measured; rates have no unit-space and reduce
  precision/unit length to fit. Detail renderers remain dormant with no A/B entry.
  Home Monitor also opens Overview. 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 (battery percentage/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
992,611 bytes and include all 3,755 GB2312 Level 1 characters. The 12-glyph
WatchTime88 resource adds 4,531 bytes, for 997,142 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 as metadata. Demo display always adds the fixed
`DEMO_TIMEZONE_OFFSET_MINUTES = 480`, including Gateway/NVS offset=0; no
timezone setting, DST, or named timezone is added. 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 VBAT is polled every three seconds outside active recording. The official
StopWatch Demo's rounded 7:1 filter and clamped 3300..4200 mV linear estimate
provide Watch BAT xx%; this is estimated SOC, not a calibrated fuel gauge. System
also shows filtered volts. Read errors/invalid values retain the latest valid
sample and increment failures. A cold boot without a valid sample shows BAT --.
One startup and at most one/minute POWER diagnostic distinguish read errors from
OK/invalid values and include VBAT, VIN, VREF, power source and charging GPIO.
Official PMG3/PMG2 charge configuration is now present. A board/driver ABI mismatch
in the Boot Gate ELF allowed IOE initialization to overwrite PM1's initialized
flag; consistent M5GFX include visibility fixes both vendor class layouts.
The new ELF must pass vendor_i2c_abi_test.py; physical voltage remains unverified.

## 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 | 3,213,968 | +1,154,128 bytes |
| merged flash | 2,125,376 | 3,279,504 | +1,154,128 bytes |
| linked flash sections | 1,937,092 | 3,090,776 | +1,153,684 bytes |
| static D/IRAM | 140,255 | 140,919 | +664 bytes |

The 5,000-glyph CJK coverage fix changes the app from 2,915,728 to 3,213,968
bytes (+298,240 bytes). The 4 MiB app partition has 980,336 bytes (23.37%)
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.

Boot Gate baseline firmware 0.6.0 artifacts (a4bf828):

- `device_lab.bin`: 3,213,968 bytes
- `merged-flash.bin`: 3,279,504 bytes
- merged SHA-256: `02c16d1c29289d7829dc7c799970c17a3b33ff5f33912a96ddd2845ba237729b`
- build command: `./scripts/build.sh m5stack-stopwatch`
- baseline flashed and Boot Gate accepted; cleanup source `48d4719` flashed and physically accepted

## 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.
- Demo display is fixed UTC+8. No timezone setting, named timezone, or DST.
- CJK dynamic coverage is bounded. Less-common glyphs use CN/TW fallback; code
  points beyond all resources show `?`.
- P2: the custom 24/28 px CJK resources remain 1-bit bitmaps; anti-aliasing is
  intentionally outside this coverage fix.
- Battery percentage is only a voltage-based estimate. Physical VBAT passed;
  charging diagnostics do not imply calibrated fuel-gauge accuracy.

## Physical acceptance handoff

The original Watch design, CJK, Battery, four-page navigation/NET, Settings Back,
UTC+8 and Voice/Approval/Network baselines are accepted for source `48d4719`.
Use the branch-specific application-only flash procedure and stop if BAT remains
--, as described in [physical-ui-cleanup-0.6a.md](physical-ui-cleanup-0.6a.md).
Do not use the main-only update-flash script for this candidate.
