Skip to content

FIRST_BOOT_QC: factory self-test + per-unit throttle calibration capture (v8.1)#118

Draft
zjwhitehead wants to merge 9 commits into
unified-everything-appfrom
first-boot-qc
Draft

FIRST_BOOT_QC: factory self-test + per-unit throttle calibration capture (v8.1)#118
zjwhitehead wants to merge 9 commits into
unified-everything-appfrom
first-boot-qc

Conversation

@zjwhitehead

Copy link
Copy Markdown
Member

Summary

Implements the FIRST_BOOT_QC design (FIRST_BOOT_QC.md, committed as the first commit of this branch): a factory QC station that runs as a blocking guided flow at the Phase 4/5 boot boundary — automatic POST framed as bus-communication tests, screen-guided per-unit throttle calibration capture, operator-confirmed output checks, a JSON QC record per unit (USB serial + persisted + readable over BLE for silent fleet sync), and a serial run_qc re-entry command.

Zero live-behavior change for the fleet: the throttle mapping stays on the fixed 0..4095 curve. Calibration is capture-only in v8.1; the mapping switch is v8.2, data-gated on the records this release collects. A potRawToPwmCalibrated() scaffold ships tested but wired to nothing.

Fleet-safety contract (the critical bit)

QC entry has exactly two paths: truly fresh NVS (new factory board) or the {"command":"run_qc"} serial command. Existing units (NVS data from ≤ v8.0) are silently back-filled qc_passed=1, never auto-calibrated, and never see the flow. Boot context is captured before refreshDeviceData() (which writes defaults on first boot and would otherwise make a fresh board indistinguishable from a fleet unit). The gate decision is a pure function covered by an exhaustive native decision-table test. No button-hold entry — button-at-boot already means perf-mode toggle.

Design decisions (Zach, 2026-07-04)

  • Screen-first, button-minimal UX: the TFT directs every step; the throttle pot is the confirmation input (squeeze past 50% span = confirm, release-gated between checks). Button used only for its own test + absent-device skip-confirms.
  • POST = bus tests: I2C (BMP3xx probe + sane pressure), SPI (MCP2515), CAN-ESC (active DroneCAN hardware-info request/response — never a throttle command), CAN-BMS (passive listen), CPU temp, factory-NVS round-trip, throttle ADC idle. LED is cue-only, unrecorded (invisible inside the case).
  • Skip-confirm pattern: ESC/BMS absent on the bench → operator button press records "skip" (bare-controller QC passes); no press → "fail" (a dead attached device can't be silently skipped).
  • Storage: new "openppg-factory" NVS namespace (15-char limit; house rule: never abbreviate) — survives user factory reset. Keys: qc_passed, qc_fw, pot_cal, pot_min, pot_max, qc_rerun, qc_record.
  • Calibration gates: span ≥ 2000, idle ≤ 800, full ≥ 3200, release-recheck tolerance; 50 Hz stability window (25 samples within 30 counts, median captured); failing capture keeps safe defaults.
  • BLE fleet sync: QC_RECORD_UUID on the config service using the proven ESC_PARAM_DATA paged-fetch pattern; the app reads it silently on connect → Supabase (app-side lands in the app repo).

Verification

  • Native tests: 84/84 (29 new in test/test_qc: gate decision table, cal gates, stability window, v8.2 mapping scaffold, JSON golden)
  • Screenshot suite (CMake/CI path): 32 pass + 4 new QC references; the 2 SplashScreen failures are pre-existing on the base branch (verified via stash) as is the pio native-screenshot env linker error on macOS
  • Embedded build: OpenPPG-CESP32S3-CAN-SP140 SUCCESS
  • cpplint (CI flags): changed files clean (5 pre-existing errors in the ESC relay files, untouched here)

Outstanding before merge/tag

plans/005-first-boot-qc-v81.md carries the 10-item HIL bench checklist (fresh unit, fleet-simulation must show zero QC UI, bare controller skip-confirms, dead-ESC fail, bad-pot rejection, BLE read-back, factory-reset survival, WDT idle) plus STOP conditions. Bench tooling: tools/qc_bench_logger.py.

🤖 Generated with Claude Code

@zjwhitehead
zjwhitehead marked this pull request as draft July 8, 2026 02:18
zjwhitehead and others added 7 commits July 18, 2026 21:25
Factory QC + per-unit throttle calibration design: automatic POST
(bus-level I2C/SPI/CAN checks), screen-guided calibration capture,
interactive operator checks, JSON traceability record, NVS gating.

Targeting v8.1 (phases 1-3 + interactive checks + serial record);
live throttle-mapping switch is v8.2, data-gated on calibration
records collected by this release.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- factory_settings: "openppg-factory" NVS namespace (survives user factory
  reset), mutex-guarded single-commit writes mirroring device_settings.
  Keys: qc_passed, qc_fw, pot_cal, pot_min, pot_max, qc_rerun, qc_record.
- qc_logic: dependency-free gate decision (fresh-NVS or serial rerun only;
  legacy units back-filled, never auto-calibrated), calibration sanity
  gates, stability-window detector, v8.2 calibrated-mapping scaffold
  (unwired), QC record + one-line JSON serializer.
- test_qc: 29 native tests incl. exhaustive gate decision table proving
  the installed fleet can never enter QC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- qcCaptureBootContext() runs BEFORE refreshDeviceData() so a fresh
  factory board is distinguishable from an existing fleet unit (which
  gets qc_passed back-filled and never sees QC).
- POST as bus-communication tests: I2C (baro probe + sane pressure),
  SPI (MCP2515 init), CAN-ESC (active DroneCAN hardware-info
  request/response — never a throttle command), CAN-BMS (passive
  telemetry listen), CPU temp, factory-NVS round-trip, throttle ADC.
- Absent-device skip-confirm: ESC/BMS not detected => operator button
  press records "skip" (bare-controller bench QC passes); no press
  => fail. Cosmetic LED cycle only — no operator LED check.
- Emits one-line JSON QC record over USBSerial + persists blob for the
  upcoming BLE fleet-sync characteristic. qc_passed only written when
  every check passes; cal/interactive checks are NOT_RUN until the
  next commits, so this intermediate stage can never stamp a pass.
- QC tuning constants centralized in shared-config.h.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nner

160x128 three-view screen: POST checklist (8 rows, name + colored
status), guided prompt (big instruction, live value in montserrat_28,
progress/countdown bar), and full-screen PASS/FAIL banner with
failed-check detail. Wired into the QC flow's view seam (serial output
kept for the bench log); button-wait now shows a live countdown.

Screenshot suite gains 4 QC states (checklist-in-progress, squeeze
prompt, passed and failed banners) with generated references, via both
the CMake harness (CI path) and the pio native-screenshot filter.

Note: SplashScreen_Light/Dark screenshot failures and the pio
native-screenshot env linker error on macOS are pre-existing on the
base branch (verified via stash) — untouched here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Release -> squeeze -> release-recheck, each auto-advancing on a 50 Hz
stability window (25 samples within 30 counts; median captured) with
live raw value + fill progress on the display — no button involved.
Sanity gates (span >= 2000, idle <= 800, full >= 3200, release-recheck
tolerance) reject bad pots; only a passing capture writes pot_min/max
to the factory namespace. The live throttle mapping is untouched —
capture-only in v8.1; the mapping switch is v8.2, data-gated.

Per-step 60 s operator timeout fails cal rather than hanging the line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The freshly calibrated throttle is the confirmation input: each cue
pulses intermittently (300/700 ms) while the operator squeezes past
50% of span to confirm observing it; 10 s timeout = fail; the pot must
return to idle between checks so one long squeeze can't blanket-pass.
The button is used only for its own functional test (single press).
No LED check — the LED is inside the case; the TFT is the feedback
surface (cosmetic LED cycle stays in POST, unrecorded).

Adds vibeDirectSet() to the vibe module: direct LEDC write bypassing
the queue, for pre-task contexts (vibeTask doesn't exist during QC).
Buzzer uses the already-direct startTone()/stopTone().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
QC_RECORD_UUID on the config service, mirroring the proven
ESC_PARAM_DATA paged-fetch pattern (app writes u32 LE offset cursor,
reads back <=240-byte chunks of the stored QC JSON record; empty read
= no record / past end). The smartphone app reads this silently on
connect and uploads to Supabase — the only fleet QC/calibration data
path once units leave the bench. App-side read + backend land in the
app/site repos.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add {"command":"run_qc"} over webSerial (sets factory rerun flag + reboot)
and tools/qc_bench_logger.py for factory JSONL capture. Ignore local plans/.

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a sticky `qc_attempted` factory flag and updates QC gate logic so failed/aborted first-boot runs re-enter QC on reboot instead of being treated as legacy fleet units. QC results are now always written (`qc_passed` true/false), rerun handling is unified, and the gate API/action names were simplified.

Refactors interactive pot-confirm behavior to use shared confirm/release thresholds with explicit release-before/after gating, and keeps safe default calibration bounds when calibration fails. Also increases QC JSON record buffer size for added IDs/check data and updates native tests to cover the new gate behavior, pot-confirm levels, and larger JSON payloads.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant