Mobile-first PWA + desktop console for the Radar Vital stack: a Seeed XIAO ESP32-C6 carrier driving a Seeed MR60BHA2 60 GHz pulsed-coherent radar that recovers heart rate, respiratory rate, presence, and ML-readiness verdicts.
The repository ships three coupled artefacts:
| Component | File | Role |
|---|---|---|
| Firmware | radar_vital_v16_5_12.ino |
XIAO ESP32-C6 + MR60BHA2 driver. Emits the v15.2 222-column CSV at 115 200 baud over USB; the first 207 columns remain the frozen v15 contract, 208-219 preserve v15.1 diagnostics, and 220-222 retain the audit fields introduced in v16.4. ENABLE_BLE stays off by default; passive module-firmware readback after radar boot/recovery lets session truthfulness record the MR60BHA2 version. |
| Trainer | radar_vital_trainer_v12_for_v16_0.py + rvt_trainer/ |
Python 3.11+ ThreadingHTTPServer. The root script is a compatibility shim over the package entrypoint. It reads the firmware CSV, manages sessions, runs preflight/ML-readiness/audit, writes live_dashboard.json, serves REST/SSE APIs, handles COM7/COM10 serial capture, and captures AiLink BLE reference data through bleak when available. |
| Dashboard | web/src/ -> radar_vital_live_dashboard_v12_for_v16_0.html |
Standalone Angular 21 + Material 3 application compiled to a committed single-file PWA artefact and www/ packages. Polls or subscribes to /api/events/subscribe, renders live KPIs, bounded waveforms/Doppler plots, alerts, reports, pairing, preflight progress, and scoped offline state. |
The mobile-first redesign plan that this branch implements is documented in AGENTS.md.
For a non-developer operator setup guide (EXE/APK/PWA pairing, placement, signal quality, troubleshooting) see docs/operator-quickstart.md.
The canonical hardware–firmware–trainer–dashboard feedback loop, shared GBR/1-D CNN experiment path, statistical outputs, and manuscript integration contract live in docs/system-feedback-loop.md.
The ranked successor-PR plan and acceptance gates are tracked in docs/v16-5-high-yield-roadmap.md.
The repository uses the documented-information and change-control procedure in
quality/qms-policy.md. It is aligned to relevant
ISO 9001:2015 controls for traceability, design change, verification, release,
monitoring, and corrective action; it does not claim certification. The
machine-readable document register and
requirements ledger bind stable IDs/revisions
to implementation and tests. Run npm run test:qms-contract -- --base-ref <PR-base> before publishing a PR.
Release builds publish qms-release-record.json and SHA256SUMS alongside the
installers. Those records bind the approved source/workflow, controlled
document register, verification state, artifact byte sizes/hashes, and signing
state. GitHub build-provenance attestations are additional evidence; none of
these records represents medical-device approval or independent conformity
assessment.
- PR72 session-data audit fixes: trainer truthfulness now measures the on-disk CSV contract width instead of loader-added columns, accepts both canonical and raw module firmware field names, runs adaptive-correction shadow metrics on suffixed 1 Hz features, runs v15 PQI shadow checks on raw radar rows, and computes BLE reference quality from time-based coverage instead of treating AiLink protocol gaps as decode failures. The BLE logger snapshots
ref_ble_summary.jsonduring capture so Windows child-process termination does not lose summary metrics. - Firmware readback:
radar_vital_v16_5_12.inopassively polls the MR60BHA2 module firmware version immediately aftermmWave.begin()and after radar recovery, so captures can populatemodule_fw_*/module_fw_valid. - PR71 live-session recovery: the trainer creates startup/standby
live_dashboard.jsonpayloads, waits longer for session start, avoids nested dashboard port conflicts, and keeps radar-only sessions when BLE is absent instead of dropping the manifest. - PR71 Home/Live UX recovery: preflight rows persist across refresh/navigation and show progress, advisory hardware/package checks no longer block Start, history infers missing timestamps/durations/subjects from session files, standby
0 bpmvalues no longer spam alerts, and Live chart/Doppler containers are bounded to stop vertical scroll growth.
Data-quality note: short captures and weak oximeter contact can still produce conditional/not-ready verdicts. For training/secondary gates, collect at least 10 minutes with stable placement and good finger contact; sessions with low HR coverage dominated by NO_PHASE/PHASE_HOLDOFF are a DSP/placement issue, not a trainer parsing failure.
# 1. Install Python deps
python3 -m pip install -r requirements.txt # pyserial, pandas, numpy, etc.
# 2. Plug in the XIAO ESP32-C6 over USB-C (or run mock mode without hardware)
python3 radar_vital_trainer_v12_for_v16_0.py serve --mock # trainer-provided mock data, no serial
python3 radar_vital_trainer_v12_for_v16_0.py serve # operator starts live sessions from the dashboard
# Equivalent package entrypoint:
python3 -m rvt_trainer serve --mock
# 3. Open the URL printed in the console
# http://127.0.0.1:8765/The first launch will register the service worker (/sw.js), wire up the manifest (/manifest.webmanifest), and prompt a PWA install on Chrome.
serve --mock publishes a sandbox preview, not an active recording. Its
/api/status response keeps active_session null until an actual capture
starts and exposes mock preview identity separately, so Stop controls and
navigation guards remain truthful. In live mode, a browser offline event marks
telemetry stale immediately; reconnecting restarts transport without changing
the bound participant/session identity.
The trainer is also pip-installable for environments where a development checkout is not convenient:
# From PyPI (once published) or directly from the repo:
pip install rvt-trainer # runtime deps, including bleak for AiLink BLE reference capture
pip install "rvt-trainer[ble]" # accepted compatibility extra; also includes bleak
# Editable install from a checkout:
pip install -e .
# The console script is then available:
rvt-trainer serve --mock
rvt-trainer --helpRequires Python 3.11+. bleak>=0.21 is part of the packaged trainer dependency set so the Windows sidecar can capture the configured AiLink oximeter without a manual BLE install.
By default the trainer binds 127.0.0.1 so nothing leaks to the network. Opt into PIN-paired LAN mode explicitly:
python3 radar_vital_trainer_v12_for_v16_0.py serve --bind lan--bind lan generates a six-digit PIN (five-minute TTL, single-use), prints the pairing page URL, and supplies a QR link encoding http://<lan-ip>:8765/?pair=<PIN>. The public /api/server-info route is metadata-only and does not serve a QR image or expose the PIN. The Windows EXE Settings card reads PIN details through the native bridge from loopback-only /api/native-pairing-info; phone/APK/PWA clients use the printed QR, /pair, or manual PIN entry. The Angular Settings view keeps the issued X-RVT-Auth token in session storage only. Five invalid PIN exchanges from one client within a minute trigger a one-minute pairing cooldown; reopen the pairing flow after the cooldown or mint a new PIN if an operator mistyped repeatedly. Protected session APIs also require an operator session token after bootstrap on local and LAN serves.
| Endpoint set | Auth | Routes (owned by rvt_trainer.api.route_registry) |
|---|---|---|
| Bootstrap/public | None | shell assets, /pair, /api/health, /api/version, /api/update/manifest, /api/server-info, /api/auth/exchange, /api/help/schema |
| EXE native loopback bootstrap | Loopback-only native bridge | /api/native-pairing-info (GET; ?format=qr adds qr_png_base64 in LAN bind) |
| Auth / operator management | Operator session token (X-RVT-Auth) |
/api/auth/validate (GET), /api/auth/login (POST), /api/auth/logout (POST), /api/auth/sse-token (POST), /api/operator-profiles (GET/POST), /api/subject-profiles (GET), /api/participants (GET/POST), /api/defaults (GET/POST) |
| PIN recovery | Recovery code (no session token needed) | /api/auth/reset-pin (POST — body: {operator_id, recovery_code, new_pin}; verifies PBKDF2 recovery-code hash; rotates code on success; separate 5-attempt/30 s lockout) |
| Host PIN reset | Loopback-only (127.0.0.1 / ::1); no token | /api/auth/host-reset (POST — body: {operator_id, new_pin}; 403 from any non-loopback address; re-mints recovery code; use for legacy profiles or when recovery code is lost) |
| Physiological / session / hardware | X-RVT-Auth operator token after bootstrap |
/api/status, /api/events/subscribe, /api/session/events, /api/session/current, /api/session/current/live_dashboard.json, /api/session/buffer, /api/sessions, /api/sessions/<id>/summary, /api/sessions/<id>/data, /api/sessions/<id>/notes (GET), /api/sessions/<id>/signoff (GET), /api/sessions/<id>/annotations (GET), /api/sessions/<id>/compare, /api/sessions/<id>/analyse/status, /api/sessions/<id>/training/status, /api/sessions/<id>/predict, /api/sessions/<id>/files/<rel>, /api/ble/scan, /api/serial/ports, /api/preflight, /api/preflight/<id> (single-check rerun), /api/trainer/log, /api/report/export |
| Study evidence | X-RVT-Auth operator token after bootstrap |
/api/study/objectives (approved manuscript objective contract), /api/study/protocol (GET/PUT; locked protocol identity), /api/study/schedule?participant_id=<P-NNN> (deterministic persisted randomization), /api/study/completion-matrix, /api/study/attempts (POST; includes no-subject denominator), /api/study/analysis (POST) and /api/study/analysis/<job> (GET), /api/study/objectives/<id>/report (GET; inconclusive until a completed analysis exists), /api/sessions/<id>/references (GET/POST; RR/temperature/HR provenance), /api/sessions/<id>/references/rr-adjudication (POST; dual-observer RR lock), /api/participants (GET/POST), /api/participants/<id> (PUT; status history), /api/subject-profiles (GET/PUT) |
| Control / mutation | X-RVT-Auth operator token after bootstrap |
/api/session/start (POST), /api/session/stop (POST), /api/session/annotate (POST), /api/session/annotations (POST), /api/participants/<id> (PUT — lifecycle status only), /api/sessions/<id>/notes (PUT), /api/sessions/<id>/signoff (PUT), /api/sessions/<id>/tags (PUT), /api/sessions/<id>/analyse (POST — rerun; returns radar_only status when reference CSV/BLE data is absent), /api/sessions/<id> (DELETE — soft-trashes to .trash/) |
Backend service ownership is split without changing the public entrypoint:
rvt_trainer.session.SessionSupervisorowns capture-process start, stop, reap, session locks, and stop markers.rvt_trainer.api.route_registryowns route names, methods, groups, and authorization policies; the compatibility handler dispatches by those names.rvt_trainer.api.commonowns strict JSON responses, stable API errors, atomic JSON persistence, and bounded process waits.rvt_trainer.monolithretains historical import aliases while downstream callers migrate;python -m rvt_trainerand the root trainer script remain equivalent packaged entrypoints.
Tokens live in the trainer's memory only — re-pair after every trainer restart.
Study sessions use a pseudonymous participant profile that is separate from the
optional physiology subject profile. A real confirmatory start must bind one
participant code to one immutable trial assignment: distance 0.6, 0.8, or 1.0 m;
barrier none or cardboard; trial 1–3; and planned duration 150 seconds.
Exploratory starts may use 0.5–1.0 m but remain explicitly labelled
exploratory, so they cannot enter confirmatory statistics accidentally.
Participant IDs and release/firmware/protocol provenance are persisted with the
session and must not be reassigned after capture starts.
The Angular dashboard keeps a checked-in route inventory in
web/src/app/services/backend-api.contract.ts; the Python test
tests/test_frontend_backend_api_contract.py fails when an API route, method,
or path is added without a matching frontend binding. Home exposes the
approved objective contract, completion matrix, withdrawal history, and
no-subject attempt ledger. Report exposes training/prediction evidence, tags,
and soft-delete controls so the four manuscript objectives can be tested from
the same operator session.
Before a real Start, the dashboard sends a compatibility handshake containing its product/dashboard version, serial protocol and width, and required API schemas. The trainer compares that request with its own release identity and known firmware truthfulness. Known mismatches return a stable conflict response with reload/restart/flash guidance; older clients without the handshake remain operational but are recorded as unverified. If a capture process fails after a session directory is allocated, its manifest remains as a failed-start record with the participant/trial, release/model provenance, failure reason, and timestamps instead of becoming an anonymous placeholder.
An explicit /api/session/stop first lets the detached capture child flush and
exit, escalates through bounded terminate/kill waits if needed, and only then
may enqueue eligible paired analysis. Stopping the control server uses the same
bounded reap but never starts analysis; already-captured files are preserved.
If the child cannot be reaped, its current-session markers remain intact and
the stop reports a failure instead of advertising an idle trainer.
Every current dashboard Start sends an idempotency_key (the equivalent
Idempotency-Key header is also accepted). The trainer reserves that key before
preflight and stores a hash of the normalized effective request. Concurrent or
response-loss retries with the same key and request replay the original result
without allocating another participant/trial directory; reusing the key with a
different request returns 409 IDEMPOTENCY_KEY_REUSED. Definitive failed starts
are replayed with their original failure/session evidence, while legacy clients
without a key retain the pre-v16.5.4 behavior for one compatibility release.
For TLS, pass --tls (auto-generates a self-signed cert under .rvt_tls/, which is git-ignored). HSTS is not sent under self-signed certs; pass --tls-trusted only when serving a CA-signed cert.
.github/workflows/pages.yml publishes the self-contained Angular dashboard shell to GitHub Pages on every push to main. Its relative manifest and service-worker paths remain within the repository Pages scope. Settings accepts a trainer origin and one-time PIN for LAN pairing.
The hosted shell cannot read serial — it's a thin client. The trainer still has to run on the operator's machine.
npm install # installs @capacitor/cli, @capacitor/android
npm run build:web # copies dashboard + assets/ into www/
npx cap sync android
npx cap open android # opens Android Studio for signing/releaseLAN HTTP traffic in the APK routes through the Capacitor native HTTP stack (via CapacitorHttp configuration in capacitor.config.ts) so the WebView's mixed-content rules never apply. Telemetry-derived offline records are segregated by demo versus live IndexedDB scope. Where local Bluetooth is available, Home exposes a Native BLE acceptance probe that validates one allowlisted AiLink notification; it is a hardware qualification check and does not replace trainer-side reference capture for a recorded session. Preflight BLE/device warnings are advisory for Start; collection can proceed as radar-only and the trainer records that status if the oximeter is unavailable.
CI: .github/workflows/build-apk.yml produces an unsigned debug APK for validation. After each accepted push to main, .github/workflows/release-artifacts.yml publishes a versioned GitHub prerelease with an APK asset and generated changelog; every release stamps its semantic version and increasing Android version code into the APK, using signing secrets when configured.
cargo install tauri-cli --version '^2.0'
npm install
npm run build:web
cargo tauri build # produces src-tauri/target/release/*.exeTauri uses Microsoft Edge WebView2 and keeps WebView network policy at connect-src 'self'. Paired LAN API/download calls run through native Rust commands pinned to the explicitly paired origin. Native BLE reference commands allowlist the configured AiLink oximeter notify profile (FFE0 service / FFE2 characteristic); Home's bounded Native BLE acceptance probe consumes that command path and reports whether a notification was received without claiming it supplied session telemetry. The EXE does not rely on Chromium Web Bluetooth prompts for local-device discovery; BLE capture is handled by the bundled Python/WinRT sidecar path. The separate radar-firmware GATT path remains disabled by default pending physical acceptance. Windows 11 ships WebView2 preinstalled; the installer uses downloadBootstrapper for other systems.
CI: .github/workflows/build-exe.yml builds the EXE on windows-latest. After each accepted push to main, .github/workflows/release-artifacts.yml attaches the NSIS installer to the versioned GitHub prerelease and generated changelog; every release stamps the same semantic version into the EXE and signs it when certificate secrets are configured.
The Python trainer keeps gradient_boosting as its default correction model and
offers an explicit, optional cnn_1d research path with causal
session-bounded windows. The CNN requires TensorFlow and enforces a
500-valid-window floor per target unless a clearly labelled experimental
override is supplied.
See docs/model-family-guide.md for commands,
data-readiness requirements, artifact behavior, and thesis-claim boundaries.
For held-out model evidence, enable --loo-eval so each run writes
outer_oof_predictions.csv with raw and causal post-processed predictions.
Generate the proposal-aligned participant-balanced report with the versioned
plan and manuscript-ready exports:
rvt-statistics --input model-gbr/outer_oof_predictions.csv \
--reference-column ref_rr --estimate-column pred_rr \
--participant-column participant_id --tost-margin 2 \
--analysis-plan quality/statistical-analysis-plan.json --confirmatory \
--out results/statistical_report.json \
--csv-out results/statistical_report.csv \
--latex-out results/statistical_report.texThe analysis plan is draft-controlled pending research/quality approval. It freezes the 0.6/0.8/1.0 m × none/cardboard × three 150-second protocol, the 1.0 m/no-cardboard primary RR TOST, five Holm-adjusted secondary tests, and the minimum-window/trial aggregation rules.
npm install
npx playwright install --with-deps chromium
npm test # runs tests/smoke/*.spec.ts
npm run test:unit:web # Angular unit/source-integrity/contrast checks
python -m pytest -q tests/test_session_data_regressions.py tests/test_trainer_audit.py tests/test_trainer_verdict.pyPlaywright covers:
- Smoke — dashboard loads, no uncaught script errors, core API routes answer, and PWA install criteria pass.
- Visual — screenshot regressions across mobile/tablet/desktop viewports and the light/dark/night/hc themes via
npm run test:visual. - API —
/api/health,/manifest.webmanifest,/sw.js,/api/server-info,/api/auth/exchange, LAN authorization, static-key denial, notes/sign-off, serial discovery, preflight, and session-analysis contracts. - Session-data regressions — PR72 pins on-disk contract width, module firmware truthfulness, adaptive-correction shadow metrics, v15 PQI shadow, and BLE reference-quality calculations against recorded session failures.
CI: .github/workflows/playwright.yml runs Python contracts, smoke/API coverage across desktop, Pixel 7, iPhone 14 and iPad projects, plus visual regression.
| Branch | Purpose |
|---|---|
main |
Operator-stable mobile-first v12 dashboard / v12 trainer / v16 firmware path. |
archive/legacy-v8-to-v11 |
Frozen snapshot of every legacy .ino / .py / .html (v8.8 → v11). Recover historical baselines from this branch only. |
Pre-mobile baseline tag: v15.0.0-pre-mobile — rollback point for the redesign work.
.
├── radar_vital_v16_5_12.ino # firmware (v16.5.12; v15.2 222-column USB contract, BLE gated off)
├── radar_vital_trainer_v12_for_v16_0.py # trainer compatibility shim
├── rvt_trainer/ # trainer package facade + legacy monolith
├── radar_vital_live_dashboard_v12_for_v16_0.html # PWA dashboard (single file)
├── assets/
│ ├── sw.js # service worker (registered at /sw.js)
│ ├── manifest.webmanifest # static fallback (trainer overrides at /manifest.webmanifest)
│ ├── icons/ # PWA icons (192, 512, 512-maskable, apple-touch)
│ ├── lib/ # self-hosted Chart.js, Hammer.js, jsqr (Phase 1a)
│ └── fonts/ # self-hosted Inter, JetBrains Mono, Material Symbols (Phase 1a)
├── android/ # Capacitor Android project (generated)
├── src-tauri/ # Tauri Windows wrapper
├── tools/extract-icons.py # Material Symbols codepoint subsetter
├── tests/
│ ├── smoke/ # Playwright smoke + API tests
│ └── visual/ # Playwright screenshot regressions
├── .github/workflows/ # CI: pages.yml, build-apk.yml, build-exe.yml, playwright.yml
├── .rvt_tls/ # git-ignored self-signed cert material
├── AGENTS.md # AI agent contract for this repo
└── README.md
Same license as upstream — see repository settings.