Skip to content

EXG (ECG/EMG/respiration) configuration: register codec, presets, live BT/BLE commands, knob editing - #70

Open
jyong15 wants to merge 10 commits into
mainfrom
claude/consensys-java-mobile-web-xmnmko
Open

EXG (ECG/EMG/respiration) configuration: register codec, presets, live BT/BLE commands, knob editing#70
jyong15 wants to merge 10 commits into
mainfrom
claude/consensys-java-mobile-web-xmnmko

Conversation

@jyong15

@jyong15 jyong15 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds complete ADS1292R (EXG) configuration support to the SDK, ported from the Java driver stack with file:line citations throughout, in four phases:

  • EX1 — register codec (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 matching isEXGUsingDefault*, resolution derived from the sensor bitmap (not the registers), and SD-header EXG bank extraction (offsets verified against ShimmerSDLog.java for both 256B and 384B headers).
  • EX2 — preset application (apply.ts): applyExgPreset porting setDefault*Configuration — preset bytes, sampling-rate→data-rate coupling (setExGRateFromFreq thresholds), sensor-bitmap resolution coupling, and conflict clearing (GSR/internal-ADCs/bridge-amp) exactly as sensorMapConflictCheckandCorrect does.
  • EX3 — live BT/BLE commands (live.ts + both clients): readExgConfig/writeExgConfig/applyExgPresetLive on Shimmer3RClient and classic Shimmer3Client, 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 the ShimmerVerObject version-code ladder for fail-fast errors on EXG-incapable firmware.
  • EX4 — knob editing (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 per setEXG2RespirationDetectFreq), 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. Version 0.1.90.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

  • EMG/test-signal wire bytes changed vs the old hardcoded 3R arrays: the old arrays carried a spurious gain on EMG's powered-down chip 2 and a non-oracle gain on the test signal. New bytes match the SensorEXG.java golden arrays; CHANGELOG carries the details.
  • The hardware-facing paths (live GET/SET round-trip, respiration/lead-off physical effects) are marked HARDWARE-VERIFY in the consuming app's QA checklist — no radio hardware was available during development; everything is oracle+mock verified.

Test plan

  • 690 tests passing (was 541 at branch point): +149 across exg codec golden vectors (Java-cited byte arrays), preset detection/application, live framing (exact bytes, both clients), knob round-trips walked programmatically against the field table, firmware-gate acceptance/rejection, and SD-header extraction.
  • tsc --noEmit clean, rollup build clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_012pin56yhefi5Mihjf3vTpL


Generated by Claude Code

claude and others added 10 commits July 21, 2026 14:02
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
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.

2 participants