Migrate firmware build to PlatformIO, restructure docs#8
Conversation
Replaces arduino-cli/mklittlefs/esptool with PlatformIO across build.sh and CI. PlatformIO parses the FS partition geometry directly from board_build.ldscript (eagle.flash.4m2m.ld) on every build, removing the hand-maintained mklittlefs size constant that caused a silent LittleFS reformat bug in the past (see docs/LITTLEFS.md). Also splits the README into a front page with a condensed quickstart, moving the full prerequisites/build-script reference to docs/BUILD.md and the WebSocket protocol spec to docs/PROTOCOL.md. Bumps version to 2.3.0.
Verified end-to-end on a real Wemos D1 Mini: firmware + LittleFS flash,
boot, WiFi/mDNS, and the full WebSocket protocol (status/ping/speed/stop)
all confirmed working via the PlatformIO pipeline. 921600 baud upload
failed intermittently ("Timed out waiting for packet header" / "Invalid
head of packet") on this USB-serial adapter; 460800 flashed reliably.
|
Full hardware verification complete — flashed to a real Wemos D1 Mini via
Ready to merge. |
… frontend builds Secrets: arduino_secrets.h (a gitignored C header, #include'd into the sketch) is replaced by secrets.ini, a gitignored PlatformIO config file merged in via extra_configs. Values become plain -D build_flags, so the firmware code needs no changes beyond dropping the #include. Verified end-to-end on real hardware: WiFi/mDNS/HTTP/WebSocket all work identically with credentials sourced from secrets.ini instead of the old header. Frontend builds: PlatformIO's own incremental compile already makes re-running `pio run` on unchanged firmware near-instant, but `npm run build` has no such skip. build.sh now checks mtimes of frontend source files against the existing build/data/ output and skips the Vite build for options 1 and 7 when nothing changed (option 2 stays an explicit, unconditional rebuild). Verified both the skip and rebuild paths.
|
Added two follow-ups, both re-verified end-to-end on hardware: Secrets → Skip redundant frontend builds. |
…gration - docs/LITTLEFS.md: replace FQBN/arduino-cli framing with the actual board_build.ldscript mechanism; fix the "find your linker script" instructions (arduino-cli command no longer applies) - docs/BUILD.md: correct the secrets.ini build_flags example to the working '"..."' quoting (was still showing the broken \"...\" form) - CONTRIBUTING.md: de-duplicate the WebSocket protocol table in favor of linking to docs/PROTOCOL.md, the single source of truth
|
Docs audit: checked all 8 markdown files (README, CONTRIBUTING, CLAUDE.md, docs/BUILD, HARDWARE, PROTOCOL, LITTLEFS, status-led-wiring) for stale references and broken links post-migration. All internal links resolve; no
|
…ch warnings - z-duino.ino: fix comment referencing the old arduino_secrets.h - Extract buildStatusJson() so broadcastStatus() and the WStype_CONNECTED handler stop duplicating the same ~10-line JSON construction - Add default: break to both webSocketEvent's and StatusLED::setState's switches, eliminating all -Wswitch warnings on unhandled enum values - StatusLED now stores the active test color and handles LED_TEST in setState(), fixing a real (if minor) bug: previously, triggering an emergency stop while in LED test mode left the LED dark afterward instead of restoring the test color, since setState(LED_TEST) hit no matching case Verified: pio run compiles with zero warnings; reflashed to hardware, confirmed boot/WiFi/mDNS unaffected and the WebSocket protocol (connect status, ping/pong, speed, stop) behaves identically to before.
…ampTo - tsconfig.json: remove the @/* path alias — nothing in the codebase uses it, every import is relative - SpeedController.vue: merge two separate import statements from the same useTrainController module into one - useTrainController.ts: rampTo() now returns a Promise that resolves when its own ramp completes (naturally or via emergency stop), replacing waitForRampComplete()'s 50ms polling loop that watched the shared rampTimer variable. toggleDirection() awaits rampTo() directly. Note: investigated removing the unused `vue-router` dependency (nothing in the app imports it) but reverted — @nuxt/ui's Link.vue override component still references it internally, and Vite's optional-peer-dep stubbing doesn't cleanly resolve its named imports (useRoute, RouterLink) during a production build with this @nuxt/ui/Vite version combo. Kept as a real (if only build-time) dependency. Verified: npm run build succeeds; exercised rampTo/toggleDirection at runtime via a stubbed-WebSocket harness (no test framework in this repo) — ramp-down, pause, direction flip, and ramp-up all sequence identically to the pre-refactor behavior, with the promise resolving at the correct point (ramp-down + DIRECTION_PAUSE).
|
Applied the simplification pass, all verified: Firmware (all four): fixed the stale `arduino_secrets.h` comment; extracted `buildStatusJson()` to remove the ~10-line duplicate between `broadcastStatus()` and the `WStype_CONNECTED` handler; added `default: break` to both switches (zero `-Wswitch` warnings now); and fixed a real minor bug found while tracing the LED_TEST warning — `StatusLED` now stores its test color and restores it correctly after an emergency-stop blink sequence, instead of leaving the LED dark. Recompiled clean, reflashed, confirmed boot/WiFi/mDNS/WebSocket protocol all unaffected. Frontend (3 of 4 applied, 1 reverted): removed the dead `@/*` tsconfig alias, merged the duplicate `SpeedController.vue` import, and replaced `waitForRampComplete()`'s polling loop with `rampTo()` returning a real `Promise` — verified at runtime with a stubbed-WebSocket harness (no test framework exists in this repo), confirming ramp-down → pause → flip → ramp-up sequences identically to before, promise resolving at the correct point. vue-router removal reverted. Looked genuinely unused from application code (confirmed: zero imports, |
…rmIO native Firmware logic was fused with hardware I/O (pinMode/digitalWrite/WiFi/socket calls alongside the actual decision logic), making it untestable without real hardware. Extracted the hardware-independent pieces into lib/ (auto- linked into both the real d1_mini env and a new native test env, confirmed via scratch testing this session): - lib/MotorLogic — the forward/reverse/stop decision (speed + direction + invert -> action), previously inline in applyMotorState() - lib/LedTiming — StatusLED's breathing-brightness and blink-timing math, previously inline in StatusLED::update() - lib/Command — WebSocket JSON command parsing/dispatch decision, previously inline in handleWebSocketMessage() (uses ArduinoJson, which is header-only and builds fine on native) z-duino.ino and StatusLED.cpp now just apply whatever these pure functions decide — identical runtime behavior. One acceptable change: malformed-JSON logging lost the specific parser error string in favor of a generic message, since that detail no longer crosses the pure/impure boundary. Added [env:native] (platform = native, test_framework = unity) plus default_envs = d1_mini so plain `pio run`/build.sh are unaffected. 38 Unity test cases across test/test_motor_logic, test/test_led_timing, and test/test_command, all passing via `pio test -e native` (no device needed). Verified: pio run (real target) still compiles clean; reflashed to the connected Wemos and re-ran the full WebSocket protocol regression (every command type, including the LED_TEST + emergency-stop combo) — no runtime behavior change from the extraction.
…tests Nearly all real logic in this app lives in useTrainController.ts (per the earlier simplification audit) — Vitest is a natural fit given the existing Vite setup. - test/setup.ts: a FakeWebSocket (happy-dom, like jsdom, doesn't implement the WebSocket API), assigned to global.WebSocket, since the composable opens a real socket at module-load time. - test/useTrainController.test.ts: speed segment/ramp logic, toggleDirection sequencing (ramp down -> pause -> flip -> fire-and-forget ramp up) using fake timers to validate the rampTo-returns-a-Promise refactor from earlier this session, direction-invert persistence via localStorage, LED brightness scaling, and reconnect backoff doubling/capping/reset. - test/SpeedController.test.ts: mount the component, verify segment coloring from a simulated status message and that E-Stop sends the right command. Needed @nuxt/ui's Vite plugin in vitest.config.ts too (not just @vitejs/plugin-vue) since UButton/UIcon are auto-imported. Test isolation: useTrainController.ts is a singleton (module-scoped state), so every test does vi.resetModules() + a fresh dynamic import rather than reusing the shared module-level state across tests. `npm test` (13 tests) and `npm run build` both verified passing.
- ci.yml: firmware job runs `pio test -e native` after compiling; frontend job runs `npm test` before the production build. - CONTRIBUTING.md / CLAUDE.md: document the new test commands, the lib/ + test/ structure, and that hardware-touching code (pins, WiFi/sockets) is intentionally not covered by unit tests.
|
Added practical test coverage to both firmware and frontend, per request. Neither had any before this. Firmware (38 Unity test cases, `pio test -e native`, no hardware needed): extracted the hardware-independent logic that was previously fused with pin/WiFi/socket calls into `lib/` — `MotorLogic` (forward/reverse/stop decision), `LedTiming` (breathing/blink timing math), `Command` (WebSocket JSON parsing/dispatch, using ArduinoJson which builds fine on native). `z-duino.ino`/`StatusLED.cpp` now just apply what these pure functions decide — identical behavior. Added `[env:native]` + `default_envs = d1_mini` so plain `pio run`/`build.sh` are unaffected (verified). Reflashed to real hardware afterward and re-ran the full WebSocket protocol regression (every command type) to confirm the extraction changed nothing at runtime. Frontend (13 Vitest tests, `npm test`): `useTrainController.test.ts` covers speed segments, the `toggleDirection` ramp/pause/flip sequencing (validates the `rampTo`-returns-a-Promise refactor from earlier, using fake timers), direction-invert persistence, LED brightness scaling, and reconnect backoff. `SpeedController.test.ts` mounts the component for two smoke tests (segment coloring, E-Stop command). Needed a `FakeWebSocket` (happy-dom doesn't implement one) and per-test `vi.resetModules()` since the composable is a module-scoped singleton. Both suites now run in CI (firmware job: compile → native tests; frontend job: `npm ci` → tests → build). Docs (`CONTRIBUTING.md`, `CLAUDE.md`) updated with the new structure and commands. |
Summary
build.shand CI. PlatformIO derives the LittleFS partition geometry directly fromboard_build.ldscripton every build (verified: packed image is exactly 2,072,576 bytes / 253 blocks, matchingdocs/LITTLEFS.md's known-good math) — this removes the hand-maintained size constant that caused a silent filesystem-reformat bug in the past.docs/BUILD.mdand the WebSocket protocol spec todocs/PROTOCOL.md.Test plan
pio runcompiles cleanly (RAM 40.0%, Flash 36.5% — sane numbers)pio run -t buildfspacks a LittleFS image at exactly 2,072,576 bytes (253 blocks, matchesdocs/LITTLEFS.md)frontend && npm run buildstill succeeds intobuild/data/./build.shoption 3 (build firmware) runs end-to-end through the new PlatformIO-backed script./build.shoption 7 to a real Wemos D1 Mini and confirm boot/WiFi/mDNS/WebSocket/UI all still work before fully trusting this in production