SomnoTrace v3.0.0
⭐ Headline feature: SomnoStage — on-device sleep staging
SomnoTrace now ships with SomnoStage, an on-device machine learning model delivering automated sleep staging directly on the ESP32-S3:
- 🧠 Oximetry-based staging: Classifies sleep stages (Wake / REM / Light / Deep) post-session using continuous finger pulse oximetry, pulse rate, and motion data.
- 📈 Interactive hypnogram: Dashboard panel featuring AASM-ordered stage rows (Wake / REM / Light / Deep) with confidence-based alpha blending.
- 🕒 Stage totals & distribution: Dynamic legend displaying duration and percentage per sleep stage for the visible chart window.
- 🏷️ Clinical disclaimer & attribution: Clear medical disclaimer and source labeling on the chart, plus a conditional SomnoStage attribution footer in Settings.
Distribution model: The public repository remains completely model-free — the encrypted model blob is fetched from a private distribution repo and injected by CI during release builds. Forks retain full SomnoTrace functionality without model dependencies. Licensing terms are documented in-repo. A C-runtime parity test (somno_ml_test) runs automatically during host tests when dev artifacts are present.
🖥️ Dashboard
- Collapsible dashboard rows: Signal rows (Flow, Pressure, Leak, SpO₂, Pulse, Sleep Stages) can now be collapsed with persistent chevron toggles for a cleaner, customizable overview.
- Smart oximetry visibility: Oximetry panels and SpO₂ therapy clamping controls now automatically hide when neither O2 Ring nor AS11 pulse oximeter streams are present.
🔧 Reliability fixes
- Session split dead zone closed (#279 — thanks @Plantucha): Gaps of 10.0–10.1 s were previously padded with ~10 s of sample-and-hold waveform instead of splitting the session. Both branches now gate directly on
SW_SPLIT_GAP_MS, ensuring no gap length falls into a dead zone between "pad" and "split". - Format-A cadence read from the file (#280 — @Plantucha): Oximetry conversion hardcoded 1 Hz in three places; it now parses the recording's own trailer (with plausibility validation and safe 1 Hz fallback), matching what the VLD3 path already did. Also cross-checks the trailer's stated sample count against the body.
- SleepHQ complete-day uploads (
atomic_day): Every SleepHQ import now carries the whole day — all session groups plusSTR.edf— so a night split by a BLE dropout can no longer leave an earlier fragment invisible after the second fragment uploads. - Upload retry-storm guard: A group that fails 5 times is parked instead of keeping its day pending forever and re-sending every sibling each cooldown cycle. It revives automatically when its files change or the day is rebuilt.
- Uploads defer during live therapy: The scheduler now skips upload passes while a session is recording, so Wi-Fi bursts never share the radio with the BLE links carrying real-time data. Pending work goes out complete right after the session ends.
🧠 Memory & telemetry
- ~23.5 KB of internal RAM recovered: The oximetry upload state table (
s_states) moved from static internal RAM to PSRAM (with internal fallback), the largest single static memory win available in the firmware. - Periodic
[MEM]telemetry every 15 minutes: Logs internal/DMA/PSRAM free, minimum, and largest-free-block, allocated-block counts, task count, and deltas against a settled[MEM-BASE]boot baseline — plus a low-stack canary warning about any task under 512 B of headroom. Leak vs. fragmentation vs. warm-up is now distinguishable straight from the log. - Deterministic SPIRAM stacks:
CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM=yis now pinned explicitly insdkconfig.defaults, ensuring the PSRAM task-stack architecture no longer relies on an IDF default.
✅ Test suite
- Mutation-driven coverage (#281, #282 — @Plantucha): Drift-direction tests for the MaskOn fallback and MaskOff paths, NULL-argument and year-2000 boundary guards, and the AS11-clock day-folder computation. Mutation survivors dropped from 16 to 11.
- Trailer host test suite (#280): New
oxyii_trailer_testhost test, including verification of 32-bit sample counts that would have silently truncated under a 16-bit read. - SleepHQ atomic-day contract test: Replays a two-fragment night against both plausible server semantics (replace-per-import and append-with-dedupe) and proves the complete-day policy keeps every fragment visible. Wired into
scripts/run_host_tests.sh.
🙏 Contributors
- Ilya Kruchinin (@ilyakruchinin) — SomnoStage integration, uploader/memory redesign, web dashboard updates, and release engineering.
- Michal Planicka (@Plantucha) — 4 merged pull requests: session split-gap threshold consistency (#279), Format-A cadence parsing (#280), and two rounds of mutation-verified test coverage (#281, #282).
Thank you both — this release is materially better for it! 🙌