Repository navigation
Epic E4: break src.ino into translation units #212
Description
Recreated from Strycher/Field_Compass-archive#212 — originally opened by @Strycher on 2026-09-05. Credentials redacted where present.
Parent feature: #151 · Plan: docs/plans/2026-08-09-platformio-hal-design.md
E4 of #151's sequence: E1 vendor config → E2 PlatformIO → E3 CI gate → E4 translation units → E5 HAL
Break src/src.ino into translation units. It is 8,712 lines in one file, which blocks parallel agent work, forces the per-issue branching override in CLAUDE.md, and prevents the Epic branch model CLAUDE-BASE mandates.
Sequencing: E3 (CI) has left #151 to become its own Feature. Owner ruling: CI lands first. E4 does not start until the CI Feature's compile-gate half is in place.
Diagnosis (task #215)
Measured, with provenance. [TOOL] = real tool output; [REGEX] = pattern-matching over the source, not yet cross-validated.
| measure | value | |
|---|---|---|
| lines | 8,712 | [TOOL] |
| functions | 206 (the plan's "162" is stale) | [REGEX] |
static functions |
44 | [REGEX] |
IRAM_ATTR functions |
0 — no ISR placement constraints | [REGEX] |
| true file-scope globals | 111 | [REGEX] |
| globals used by 1–2 functions | 41 (37%) | [REGEX] |
| hot-core globals (≥15 functions) | 8 | [REGEX] |
| functions touching no global | 44 | [REGEX] |
| rely on generated prototypes | 70 of 206 (33%) | [REGEX] |
| dead globals | none | [REGEX] |
Hot core: gpsData (19), sdAvailable (18), framAvailable (16), useMetricUnits (16), settingsSubScreen (15), currentScreen (15), useFahrenheit (15), cacheListCount (15).
Hard blocker found by diagnosis
Five state blobs are anonymous structs with a single instance — struct { bool valid; float latitude; ... } gpsData; — namely gpsData (113 refs), envData (58), imuData (35), shtData (29), weatherTrend (24).
extern cannot name a variable of anonymous struct type. Putting the anonymous struct in a header gives each TU a distinct type plus duplicate definitions; extern decltype(gpsData) gpsData; is circular. Naming them is a prerequisite, and it touches declarations only — all usage sites are unchanged.
Structure — ownership first, no globals.h
Owner ruling: no transitional externs header at any point. Each unit takes owned state with it, so ownership precedes extraction.
Stage 1 — prerequisites, no extraction
- 1.1 name the 5 anonymous structs
- 1.2 file-scope
#defines →include/fc_config.h - 1.3 type declarations →
include/fc_types.h - 1.4 cross-validate the
[REGEX]inventory againstnm, and build the per-extraction harness
Stage 2 — hot-core ownership. Peripherals {sdAvailable, framAvailable}, UiState {currentScreen, settingsSubScreen}, UnitPrefs {useMetricUnits, useFahrenheit}, geocache unit {cacheListCount}. Behaviour-neutral, still inside src.ino.
Stage 3 — vertical slices, cheapest first. Each takes its state with it.
| unit | funcs | lines | externs | exports |
|---|---|---|---|---|
ui_widgets |
14 | 506 | 0 | 9 |
sd_health |
5 | 154 | 1 | 4 |
logging — must be early regardless |
12 | 271 | 12 | 7 |
geocache_io |
11 | 342 | 14 | 8 |
oled |
6 | 226 | 16 | 1 |
storage_fram |
8 | 245 | 22 | 7 |
sensors |
10 | 379 | 23 | 8 |
logging ordering is not negotiable: logPrintf has 52 callers, logPrintln 40, sdOpenSafe 14 — bottom of the dependency stack.
Stage 4 — heavy consumers and screens. weather 638 / web 1,063 (47 externs but only 1 export, a leaf consumer) / screen_settings 1,490 / screen_geocache 1,337 / lvgl_core 794 / screen_telemetry 330 / screen_environment 201 / screen_compass 76.
Residue: ~567 lines (setup, loop, buttons, display sleep) stay in src/src.ino.
Per-extraction verification
Proven on #186: identical size totals do not prove behaviour-neutrality — symbol set, sizes and types were identical across 14,958 symbols while 608 addresses shifted, because link order follows TU name.
pio run -e feather_s3
xtensa-esp32s3-elf-nm --print-size --defined-only .pio/build/feather_s3/firmware.elf | sortEach extraction asserts: build exits 0; RAM/Flash totals unchanged or the delta explained; symbol set, sizes and types identical; address shifts reported not hidden; arduino-cli compile ... src/ still builds while #161 is open; hardware verification before merge.
Exit criteria
- No translation unit exceeds ~1,500 lines
- No
globals.hof externs exists at any point - Parallel agent work is possible — two agents can edit different units without conflict
- The per-issue branching override in CLAUDE.md is re-evaluated, since its only justification was this file
Stages become Tasks once the owner grants them. Full plan: cheerful-enchanting-pearl.md.
Activity
docs(#215): inventory the god object from the linker, not from regex
Strycher commentedon Sep 7, 2026
The
[REGEX]table in this Epic's body is superseded bydocs/god-object-inventory.md(#215, mergedc1c58a9), measured from the linker rather than the source. Not editing the body — it is the owner's — but nothing in that table should be planned against now.webServer(24 users) was missing and is the most-referenced variable in the fileIRAM_ATTRfunctionstouchDetected, write-onlyThe Stage 2 and Stage 3 tables in the body are scoped against the 111 figure and need re-deriving from the document's Appendix B (the 35-variable cut set) before any Task is created from them.
Strycher commentedon Sep 8, 2026
Session record for the autonomous run of 2026-09-07 (bands 2–4 of this Epic), for anyone who needs the why later.
State. Bands 1–3 merged (#266–#269, #275–#279):
src/src.inoonmainis 4,387 lines beside 17 domain units. Band 4 is PR #280, open and not merged: #265 requires hardware verification before merge and nothing in E4 was flashed. After it,src.inois 750 lines ofsetup/loop/initLVGL/buttons and the firmware is 25 translation units.Authority. Bands 2–3e were merged by the agent under the owner's standing grant of that day, each after
compile-gateand a passingnmproof, each with an approval comment naming the grant. The grant is quoted verbatim, with the agent's confirmed reading, indocs/e4-session-2026-09-07.md(PR #281).Decisions made under the grant, for the owner's review (each in its PR body and in the session record §3): band 3 split into #270–#274 by dependency layer with the web unit last, against #264's "webServer first";
initSerialLog(bool rtcOk);fc_config.has the pin map andfc_version.hforFW_VERSION;framFlushToSDinweather;ui_statestate-only until band 4; atouchunit; the WiFi state and credential placeholders private toweb.cpp;initLVGLstaying as the composition root; band 4 as one open PR rather than five merged children.Two corrections to the #215 diagnosis: the relocation graph never counted read-only data (fixed; the cut set did not change), and
gnrmcFixThisCycleis write-only dead state — deleting it is the owner's call.Set aside for the owner: hardware verification and the merge of #280; closing #264 and #265; this Epic's integration test and close; the six dead functions in inventory §8; #99; relaxing CLAUDE.md's parallel-agent rule.
Evidence:
docs/god-object-inventory.md§8–§15 (per band),docs/e4-proofs/(verbatim verifier output per extraction, PR #281), the commit messages, and each PR body.docs(#212): the E4 session record, the proofs, and the facts CLAUDE.m…