diff --git a/doc/plan/signals.md b/doc/plan/signals.md new file mode 100644 index 0000000..a286cf7 --- /dev/null +++ b/doc/plan/signals.md @@ -0,0 +1,68 @@ +# Plan: Decode wmedf signal data, expose via CLI and JSON export + +## Context + +The .wmedf parser currently extracts only the session start timestamp. The binary signal data (18 channels of continuous waveforms — pressure, respiratory flow, SpO2, heart rate, etc.) is ignored. This data is clinically critical: it enables visual confirmation of apnea types, desaturation tracking, and cardiac correlation. + +## Binary format summary + +- Standard EDF, 18 channels, 4864-byte header (256 global + 18×256 signal) +- Signal headers use EDF interleaved layout (all labels, then all transducers, etc.) +- Data records: 70 bytes each (35 × i16 LE), 18 seconds per record +- Samples per record varies by channel: Pressure(5), RespFlow(10), SPRstatus(5), all others(1) = 35 total +- Num records: derive from `(file_size - 4864) / 70` (header field unreliable) +- Conversion: `physical = phys_min + (raw - dig_min) * (phys_max - phys_min) / (dig_max - dig_min)` + +## Files to modify + +- `crates/tidal-core/src/entities.rs` — move `sample_rate_hz` to `SignalChannel`, add new `SignalLabel` variants +- `crates/tidal-devices/src/lowenstein/wmedf.rs` — full signal header + data record decoding +- `crates/tidal-store/src/sqlite.rs` — use per-channel sample rate in read/write paths +- `crates/tidal-cli/src/main.rs` — add `--signals` flag to `session` command +- `crates/tidal-cli/src/export.rs` — add signal summaries to JSON export + +## Step 1: Update entities + +Move `sample_rate_hz` from `SignalBlock` to `SignalChannel`. Add `SignalLabel` variants: `EEPAPTarget`, `TotalLeakage`, `RSBI`, `AMV`, `SPRStatus`. Update `Display`/`FromStr`. + +## Step 2: Update tidal-store for per-channel sample rate + +- Write path: use `ch.sample_rate_hz` instead of `block.sample_rate_hz` +- Read path: store rate into each `SignalChannel`, construct `SignalBlock { channels }` without block-level rate +- Update tests + +## Step 3: Implement wmedf signal decoder + +In `wmedf.rs`: + +1. Parse signal headers from the interleaved EDF layout (labels at 256, units at 1984, phys min/max, dig min/max, samples_per_record at 4144) +2. Map EDF label strings to `SignalLabel` variants (e.g. "EEPAPsoll" → `EEPAPTarget`, "HeartFrequency" → `HeartRate`) +3. Parse data records: iterate 70-byte chunks, read i16 LE values, distribute across channels by samples_per_record +4. Convert digital → physical values per the EDF formula +5. Compute per-channel sample_rate_hz: `samples_per_record / 18.0` (record duration) +6. Build `SignalBlock` with populated `SignalChannel` entries + +## Step 4: Add `--signals` to CLI session command + +When `--signals` is set, load session with signals and display summary table: +``` +Signals 18 channels + Label Unit Samples Rate(Hz) Min Max Mean + Pressure hPa 3630 0.278 6.0 12.3 8.4 + RespiratoryFlow l/min 7260 0.556 -42.0 55.3 1.2 + SpO2 % 726 0.056 88.0 97.0 93.2 + ... +``` + +## Step 5: Add signal summaries to JSON export + +Add `--signals` flag to export command. When set, load sessions with signals and include per-channel summary (label, unit, sample_rate_hz, sample_count, min, max, mean) in JSON output. Raw samples excluded from JSON — too large. + +## Verification + +1. `cargo build` — no errors or warnings +2. `cargo test` — all tests pass (including updated signal_blob_round_trip test) +3. `rm ~/.local/share/tidal/tidal.db && tidal import ~/lowenstein/therapy_extracted --from 2026-03-26 --to 2026-03-28` — imports with signals +4. `tidal session 300306-003344 --signals` — shows 18 channels with plausible values +5. `tidal export --format json --from 2026-03-26 --to 2026-03-28 --signals` — JSON includes signal summaries +6. Verify: Pressure values in hPa range (4-20), SpO2 in % range (80-100), HeartRate in bpm range (40-120)