EXG (ECG/EMG/respiration) configuration: register codec, presets, live BT/BLE commands, knob editing - #70
Open
jyong15 wants to merge 10 commits into
Open
EXG (ECG/EMG/respiration) configuration: register codec, presets, live BT/BLE commands, knob editing#70jyong15 wants to merge 10 commits into
jyong15 wants to merge 10 commits into
Conversation
Add a pure EXG (ECG/EMG/respiration) register codec under src/devices/exg/: decodeExgRegisters / encodeExgRegisters (10-byte per-chip bank ↔ structured labelled settings, must-be bits enforced), detectExgPreset (tolerant match, ecg/emg/test-signal/respiration/custom/off), and exgResolutionFromSensors (16/24-bit from the sensor bitmap, not the registers). Ported from SensorEXG.java, ExGConfigBytesDetails.java, and ShimmerObject.exgBytesGetConfigFrom. Also extract the exg1/exg2 register banks from the SD-log header (bytes 56-65 / 66-75, ShimmerSDLog.java:252-253/322-323) into SdLogHeader. Golden-vector round-trip tests against the Java preset arrays + the hardcoded 3R arrays, must-be-bit enforcement, resolution/bitmap derivation, and malformed-length rejection. Version 0.1.10-exg.1 for vendoring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012pin56yhefi5Mihjf3vTpL
Add src/devices/exg/apply.ts — the write half of EXG configuration that builds on EX1's codec/detection. applyExgPreset(input, preset, resolution) turns a chosen preset (ecg/emg/test-signal/respiration/off) + resolution (16/24-bit) into the two register banks plus the enabled-sensors bitmap, porting the Java preset setters SensorEXG.setDefault* as a whole: - resolution lives only in the sensor bitmap (setExgChannelBitsPerMode); register bytes are identical for 16- and 24-bit. EMG is chip-1-only, every other preset is both chips, respiration is NOT forced to 24-bit. - sampling-rate -> REG1 data-rate on both chips via exgRateSettingFromFreq (port of setExGRateFromFreq, <= thresholds), distinct from the live-BT getOversamplingRatioADS1292R which is not used on the docked path. - sensor-conflict clearing (exgConflictingSensors + clear from the bitmap) matching desktop's sensorMapConflictCheckandCorrect (disables, not warns). - CHIP1 oscillator-clock-joined bit set for Shimmer3R, else preserved. 'off' zeroes both banks and clears all four resolution flags. New exports: applyExgPreset, exgConflictingSensors, exgRateSettingFromFreq, EXG_CONFLICTING_SENSORS + ExgApplyInput/ExgApplyResult/ApplicableExgPreset. Golden-vector, bitmap-coherence, rate-threshold, conflict, idempotence and detect round-trip tests (50 new, 624 total). Version 0.1.10-exg.2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012pin56yhefi5Mihjf3vTpL
…y (EX3)
Generalise the Shimmer3R hardcoded 16-bit EXG writers into a codec-driven
live path, add GET read-back, and extend both to the classic Shimmer3Client.
- New pure framing module src/devices/exg/live.ts: buildGetExgRegsCommand
({0x63,chip,0,10}), buildSetExgRegsCommand ({0x61,chip,0,10,reg0..reg9}),
decodeExgRegsResponse (12-byte [0x62][echo][reg0..reg9]),
exgBanksEqualIgnoringStatus (skips read-only REG8). Ported byte-for-byte
from ShimmerBluetooth.readEXGConfigurations/writeEXGConfiguration
(ShimmerBluetooth.java:4023,4220,468,1641-1652).
- readExgConfig() / writeExgConfig(exg1,exg2) / applyExgPresetLive(preset,
resolution) on both Shimmer3RClient and Shimmer3Client. Enabled sensors
written LAST (ShimmerBluetooth.java:2732,2735; EXG regs earlier :2670).
- Write-safety deviation from Java (fire-and-forget + timeout-disconnect,
:4028-4034): SET -> await ACK -> GET read-back -> compare -> throw on
mismatch.
- 3R injects the ADS1292R oversampling ratio into REG1 (getOversampling-
RatioADS1292R); classic writes banks verbatim. enable*16Bit now delegate
to applyExgPresetLive (hardcoded arrays dropped).
- Classic adds EXG_REGS_RESPONSE (0x62, 11 payload) to the response-length
table so the unframed byte-stream parser frames it.
- EXG GET/SET/apply refused while streaming (read-back needs control plane).
- Tests: exg/live framing; per-client GET/SET framing, read-back decode,
read-back-verify mismatch throw, applyExgPresetLive end-to-end, streaming
guard (+18 tests, 642 total).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012pin56yhefi5Mihjf3vTpL
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012pin56yhefi5Mihjf3vTpL
Live applyExgPresetLive('off') previously reused the docked
applyExgPreset('off'), pushing all-zero register banks through
writeExgConfig; the ADS1292R forces its must-be bits on write
(ExGConfigBytesDetails.java:507-525) so the zeroed banks fail
read-back-verify on compliant firmware. The live path now skips the
register write and clears only the EXG resolution bits via setSensors,
matching Java's live-disable flow (writeEnabledSensors is the disable
path, ShimmerBluetooth.java:2732,2735; readEXGConfigurations /
writeEXGConfiguration run only while EXG stays enabled, :2670,4010-4014).
The docked InfoMem path keeps zeroing the banks (passive storage; EX1's
detectExgPreset keys 'off' off all-zero banks). New export
clearExgResolutionFlags.
Shimmer3Client._assertExgSupported now ports the Java firmware gate
verbatim -- (internal >= 8 && code == 2) || code > 2
(ShimmerBluetooth.java:4011) -- deriving the firmware-version code from
the parsed FW version + hardware id exactly as ShimmerVerObject does
(ShimmerVerObject.java:266-311), and throws a clear error instead of
hanging to timeout on EXG-incapable firmware. New exports
deriveShimmer3FirmwareVersionCode, shimmer3SupportsExg.
Tests: live 'off' on both clients issues no 0x61 traffic and clears the
EXG bitmap, against a mock that enforces the must-be bits on echo (which
also proves a zeroed writeExgConfig throws read-back mismatch); old
BtStream (code 1) and BtStream 0.2.x below internal 8 (code 2) are
rejected before any radio traffic, new firmware accepted; encode(decode)
clears set LOFF_STAT bits; detectExgPreset with chip1-only sensors is
not respiration (both-chips gate).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012pin56yhefi5Mihjf3vTpL
CHANGELOG notes for the EX post-review round: the live 'off' read-back fix, the fast-fail firmware gate, and a Changed/BREAKING-behaviour note for the EMG/test-signal preset byte changes vs the old hardcoded 3R arrays (EMG chip-2 REG4/5 0xE1->0x81; test-signal REG4/5 0x15->0x05, matching the SensorEXG.java golden arrays, SensorEXG.java:1813-1814 / :1845). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012pin56yhefi5Mihjf3vTpL
deriveShimmer3FirmwareVersionCode and shimmer3SupportsExg are used by Shimmer3Client._assertExgSupported and are useful for callers that want to check EXG support before offering the UI; surface them from the package entry point (the CHANGELOG already lists them as new exports). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012pin56yhefi5Mihjf3vTpL
Add the per-knob editing layer the docked config editor stages individual EXG settings through, complementing EX2's whole-preset apply. New module `src/devices/exg/knobs.ts`: - `updateExgSetting(banks, field, value)` and batched `applyExgKnobEdits` return new banks with one field changed, the must-be bits re-enforced, and every other bit preserved. Field addressing is explicit per chip: the four gain knobs name chip+channel (exg1Ch1Gain … exg2Ch2Gain), rate and lead-off current/threshold write BOTH chips, reference electrode writes the CHIP1 REG6 nibble, respiration knobs are CHIP2-only — mirroring the SensorEXG setters behind setConfigValueUsingConfigLabel (SensorEXG.java:2314-2635). - `respirationPhaseOptions(freq)` ports the freq-dependent phase lists (16 steps @32 kHz values 0-15 / 8 @64 kHz values 0-7, SensorEXG.java:143-146). `updateExgSetting` REJECTS an illegal phase-for-frequency (ExgKnobValueError); changing the frequency auto-remaps the phase to that frequency's canonical default (112.5°/reg 10 at 32 kHz, 157.5°/reg 7 at 64 kHz), the net effect of setEXG2RespirationDetectFreq (SensorEXG.java:2618-2628). - Respiration-locked-unless-enabled: respirationFrequency/phase edits throw ExgRespirationLockedError unless chip-2 demod+mod are on, matching the desktop greying-out (PanelAdvancedExG.java:340,357-361,416-420). The respirationEnable toggle is never gated and can unlock the others within a single batch. - Lead-off detection macro ports setEXGLeadOffCurrentMode Off/DC (SensorEXG.java:2495-2545); AC current and the three-unipolar branch are descoped (EX4). - Typed errors: UnknownExgKnobError, ExgKnobValueError, ExgRespirationLockedError, RangeError for wrong bank length. Must-be bits always enforced on output. - Reuses registers.ts as the single source of truth via new exports `readExgField` / `setExgFieldPreserving` / `ExgFieldName`, plus `REFERENCE_ELECTRODE_OPTIONS` / `LEAD_OFF_DETECTION_LABELS` option lists (no enum data duplicated). Tests: tests/exg/knobs.test.ts (+39) walks every knob programmatically (set → decode → other banks untouched), the exact phase-by-freq lists, illegal phase rejection, freq-flip remap, respiration gating, chip addressing, batch ordering, and readExgKnobs↔updateExgSetting idempotence. Also drops an unused import flagged by lint in tests/dock/rtc-config-write.test.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012pin56yhefi5Mihjf3vTpL
CHANGELOG note for EX4 (per-knob EXG editing): updateExgSetting / applyExgKnobEdits, the freq-dependent respiration phase lists and freq-flip auto-remap (112.5°@32 kHz / 157.5°@64 kHz), the respiration-locked-unless- enabled contract, the lead-off detection macro, and the typed validation errors. Bump package.json to 0.1.10-exg.5. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012pin56yhefi5Mihjf3vTpL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds complete ADS1292R (EXG) configuration support to the SDK, ported from the Java driver stack with file:line citations throughout, in four phases:
src/devices/exg/registers.ts,presets.ts): decode/encode of the two 10-byte register banks into labeled settings (data rate, per-channel gain/mux, reference electrode, lead-off, RLD, respiration, test signal), must-be-bit enforcement, tolerant preset detection matchingisEXGUsingDefault*, resolution derived from the sensor bitmap (not the registers), and SD-header EXG bank extraction (offsets verified againstShimmerSDLog.javafor both 256B and 384B headers).apply.ts):applyExgPresetportingsetDefault*Configuration— preset bytes, sampling-rate→data-rate coupling (setExGRateFromFreqthresholds), sensor-bitmap resolution coupling, and conflict clearing (GSR/internal-ADCs/bridge-amp) exactly assensorMapConflictCheckandCorrectdoes.live.ts+ both clients):readExgConfig/writeExgConfig/applyExgPresetLiveonShimmer3RClientand classicShimmer3Client, with oracle-verified 0x61/0x62/0x63 framing (the response's chip byte is an ignored echo; chip identity is tracked host-side). Deliberate safety deviation from Java's fire-and-forget: SET → ACK → GET read-back → compare → throw on mismatch. 3R oversampling injection vs classic verbatim write both ported. Firmware gate ports theShimmerVerObjectversion-code ladder for fail-fast errors on EXG-incapable firmware.knobs.ts): single-field updates preserving all other bits, respiration phase↔frequency coupling (16 phases at 32 kHz / 8 at 64 kHz, auto-remap on frequency flip persetEXG2RespirationDetectFreq), respiration-locked-unless-enabled gating, typed validation errors.Two adversarial review rounds ran over this line; all findings fixed (notably: live "Off" now clears sensor bits without writing zeroed banks, matching Java's disable flow — zeroed banks violate the chip's must-be bits and would fail read-back-verify).
Also merges the current
main(Verisense DEV-874/366/809/900 line) — 690 tests green on the combined tree. Version0.1.9→0.1.10-exg.5(prerelease line used by the app's vendored tarballs; the next release cut can collapse this to a clean version).Behavior notes for reviewers
SensorEXG.javagolden arrays; CHANGELOG carries the details.Test plan
tsc --noEmitclean, rollup build clean.🤖 Generated with Claude Code
https://claude.ai/code/session_012pin56yhefi5Mihjf3vTpL
Generated by Claude Code