Skip to content

Native OTB device sources (Muovi/Muovi+ & Quattrocento)#6

Open
RaulSimpetru wants to merge 28 commits into
mainfrom
feat/otb-device-sources
Open

Native OTB device sources (Muovi/Muovi+ & Quattrocento)#6
RaulSimpetru wants to merge 28 commits into
mainfrom
feat/otb-device-sources

Conversation

@RaulSimpetru

@RaulSimpetru RaulSimpetru commented Jun 4, 2026

Copy link
Copy Markdown
Member

What

Re-adds the ability to connect to OT Bioelettronica EMG hardware directly from MyoGestic 2.0 — a capability that existed pre-2.0 (via the Qt biosignal-device-interface library) but was dropped in the ground-up rewrite.

Implemented as native, pure-Python (no Qt) acquisition Source classes under myogestic/sources/otb/:

  • MuoviSource (Muovi / Muovi+) — PC is the TCP server on :54321; big-endian int16 (EMG) / int24 (EEG); 286.1 nV/LSB. Muovi+ = identical to Muovi with 64 biosignal channels (same 6 aux) → 70 total.
  • QuattrocentoSource — PC is the TCP client to 169.254.1.10:23456; little-endian int16; 40-byte CRC-8 config; 120/216/312/408 channels (96/192/288/384 biosignal).

Both fit the existing pull-based Source protocol (connect → StreamInfo, non-blocking read → (data, ts), disconnect), so they drop straight into a Stream. No new runtime dependencies (stdlib socket + numpy).

Protocol verification

Cross-checked against manufacturer PDFs and OT Bioelettronica's own MATLAB reference code (vendored, reference-only, under docs/reference/otb/):

  • Muovi TCP Protocol v2.4, SyncStation v2.8, MuoviPro v5.1, MuoviLite v1.1, Quattrocento Configuration Protocol v1.7 + Read_muovi.m, Read_Quattrocento.m, CRC8.m.
  • Caught that the old bdi library encoded the Muovi control byte incorrectly — we follow the manufacturer formula (EMG<<3)|(mode<<1)|GO.

Review

  • Built task-by-task (TDD) with spec + code-quality review at each step, a final whole-implementation review, and an independent second-opinion review.
  • That review caught a real bug: Quattrocento was treating the 16 AUX IN channels as biosignal and mis-scaling them — fixed (default bio 96/192/288/384; AUX→V, accessory→raw), plus a recv() error-path socket-cleanup gap.

Tests

Notes

  • Includes a small chore: gitignore stray lsl-dummy-stream binary commit that rode along from the same working session.
  • Docs: docs/how-to/connect-otb-devices.md, example examples/otb/muovi_emg.py, design spec + plan under docs/superpowers/.
  • Follow-ups (scoped in the spec): SyncStation multi-probe path, an in-GUI device-config panel, and Sessantaquattro. A TEST-mode (ramp) first-connect sanity check is recommended on real hardware, but device geometries are now confirmed against manufacturer docs.

The Rust lsl-dummy-stream (Zarr-replay tool from the separate
lsl-recording-toolbox repo) is not a MyoGestic dependency — nothing in the
package, examples, tests, or docs references it. MyoGestic's EMG simulator
is the cross-platform Python myogestic.tools.emg_generator. Ignore the path
so the platform-specific binary can't be committed by accident.
Pure-Python (no Qt) Source implementations for OT Bioelettronica devices,
re-adding pre-2.0 hardware connectivity. Captures the exact wire protocol
(socket roles, config bytes, frame formats, endianness, conversion factors,
Quattrocento CRC-8) extracted from biosignal-device-interface, plus the
shared-base architecture, data contract, GUI integration, and a phased
hardware-validated test plan.
- Conversion factor: use the physically-correct gain->resolution mapping
  (gain-8 = 286.1 nV finer, gain-4 = 572.2 nV); bdi's dict had these swapped.
  OTB's protocol PDF is not public, but the 8:4 gain ratio settles it.
- Promote an in-GUI device-config panel into scope: a source-agnostic
  config_spec()/set_config() optional Protocol extension + a generic
  device_config ImGui widget (Apply & Connect via reconnect()), plus a minimal
  manual-connect acquire-loop change behind a per-stream flag.
Cross-checked §5.1 against OTB Muovi TCP Protocol v2.4, SyncStation TCP
Protocol v2.8, and MuoviPro User Manual v5.1:
- Replace bdi's control-byte formula with the OFFICIAL layout
  (emg_eeg<<3 | mode<<1 | go); bdi encoded it differently.
- Conversion factors confirmed (gain-8 286.1 nV, gain-4 572.2 nV) + input
  ranges + firmware HPF (Fsamp/190).
- Socket role confirmed (PC = TCP server :54321; AP/DHCP detail; ~1400-byte
  TCP chunks).
- Name the 6 aux channels (IMU quaternion WXYZ, buffer usage, sample counter).
- Add §11 SyncStation multi-probe path (PC=client 192.168.76.1:54320, CRC-framed
  command strings) as a future addition on the same base.
- Flag Quattrocento (§5.2) as bdi-sourced/provisional pending its OTB PDF, and
  note TEST-mode ramps as the first-connect endianness/byte-layout check.
MuoviLite User Manual v1.1 closes the last open items:
- Byte order EXPLICITLY confirmed big-endian (§8.1.2) for both 16/24-bit;
  no longer an open question.
- LSB = ADC_RANGE/2^24 = 286.1 nV confirms the gain-8 factor; 16-bit EMG keeps
  that LSB (range 18.75 mV).
- All 38 channels (bio + 6 aux) share the active resolution (16-bit EMG /
  24-bit EEG) -> decoder reads one uniform width per frame.
- Channel map: 33-36 IMU quaternion WXYZ (BNO055 NDOF, 100 Hz held), 37 buffer
  usage + trigger, 38 sample counter; emit aux unscaled when include_aux.
- Note OTB MATLAB demo code as the canonical decode reference.
Cross-checked §5.1/§11 against OTB MATLAB Read_muovi.m v3.0, Read_muoviAP.m
v2.0, and CRC8.m:
- Command = EMG*8 + Mode*2 + 1 and stop = Command-1 confirm the official
  control byte (bdi superseded); ConvFact 0.000286, tcpserver big-endian.
- Mode table corrected: channel count varies by mode (NumChanVsMode=[38 22 38
  38]); Mode 1 = "16-ch" in MATLAB v3.0 vs "gain 4" in PDF v2.4 -> firmware-
  dependent, avoid in v1. StreamInfo.n_channels must derive from (device, mode).
- ch37 trigger/buffer unpacking (bit15 trigger, low 15 buffer); ch38 counter.
- SyncStation framing confirmed: START=(SIZE<<1)|GO, per-device
  (DEV<<4)|(EMG<<3)|(Mode<<1)|EN, CRC8 trailer, stop=[0,CRC8].
- Plan to vendor the .m references into docs/reference/otb/ during build.
Reference-only (not imported/shipped) manufacturer example scripts that the
native OTB sources will mirror: Read_muovi.m (direct probe), Read_muoviAP.m
(SyncStation), CRC8.m, plus a README distilling the verified protocol facts.
Cross-checked §5.2 against Quattrocento Configuration Protocol v1.7 and OTB
MATLAB Read_Quattrocento.m v5.0 (+ CRC8.m, identical poly 0x8C):
- ACQ_SETT bit layout confirmed; per-input 3-byte config (CONF0 muscle /
  CONF1 sensor+adapter / CONF2 side|hpf|lpf|mode) with 0-based bit values
  (mode 00=mono/01=diff/10=bipolar; default CONF2=0x14).
- TCP client 169.254.1.10:23456, little-endian int16 confirmed.
- Channel counts 120/216/312/408; accessory channels are the last 8 (counter
  @ n-7, trigger @ n-6, buffer @ n-4); conversion factors confirmed (bio
  5.086e-4 mV, AUX IN 1.526e-4 V, accessory raw).
- Stop = byte0 0x80 + recomputed CRC (always stop before reconnect).
- §5.2/§10 promoted from provisional to manufacturer-verified.
- Vendor Read_Quattrocento.m, FourMapsQuattrocento.m,
  ReadFromOTBiolabLightQuattrocento.m into docs/reference/otb/.
TDD, bite-sized tasks: CRC8, pure decoders (big/little-endian, int24),
device constants + command builders, shared _OTBSource base (socket
buffering/framing/timestamps), MuoviSource (TCP server) + Quattrocento
(TCP client), loopback tests over real sockets, example + how-to docs.
GUI device-config, SyncStation, and Sessantaquattro deferred to follow-up
plans.
Wrap the fake-probe thread body so sendall/recv after the source closes the
socket no longer print a spurious traceback. No change to assertions.
Code-quality review fix: the crc8 re-export was imported mid-file with a
suppressed E402; _crc is a leaf module with no circular-import risk, so it
belongs with the top-of-file imports.
…ames)

- H2: empty recv (peer close) now drops the socket after flushing buffered
  frames, so the acquire loop no longer spins on a dead connection.
- H3: connect()/accept_and_start() clean up the socket if _send_start raises.
- H1: fix off-by-one in Quattrocento accessory channel names (counter/trigger/
  buffer were one position too high vs Read_Quattrocento.m RampChan/BuffChan).
- M2/M4: close any stale server/client socket on re-listen / reconnect.
- M1/L1: fix misleading loopback test comment; move example import to top.
- Add regression tests for the peer-close and start-failure paths.
…g (Codex)

Independent Codex review caught two real bugs prior reviews missed:
- Quattrocento default treated the 16 AUX IN channels as biosignal and scaled
  them with the EMG mV factor. Default bio is now per-mode 96/192/288/384
  (= streamed - 16 AUX - 8 accessory); include_aux scales AUX IN to V and emits
  the 8 accessory channels raw. Channel names label bio/aux/accessory correctly.
- _base.read() recv() OSError now drops the socket (and flushes buffered frames)
  instead of polling a dead connection forever.

Tests: add Quattrocento partition + AUX-scaling + biosignal-only cases, an
OSError-drop case, and assert the Muovi start control byte (0x09) in the
loopback. Docs updated with default biosignal counts. 25 OTB tests pass.
Muovi+ is identical to Muovi but with 64 biosignal channels (same 6 aux, control
byte, and big-endian format) -> 70 total / 38 for the 32-ch mode. The 68/36
figures only arise via the SyncStation path (§11), which packs channels
differently. Resolves the prior "direct Muovi+ geometry unverified" note;
implementation already matched (muovi_geometry(plus=True) -> 64+6=70).
Merged main (PR #8: library reorg, naming standardization, ruff D / ty-clean,
Windows session fix, doc/example test harness) into the OTB device-sources branch
and brought the PR up to date:

- API: window_seconds= -> window_ms= in the OTB example + connect-otb-devices.md
  (StreamInfo/Source usage was unchanged, so the sources needed no edits).
- ruff (new D + rules now apply to OTB code): package docstring for sources/otb,
  D400 period fix in _constants, disconnect() docstring in muovi; gave tests the
  same E402 import-placement latitude examples already have; dropped an unused
  import.
- ty (repo is ty-clean now): narrowed the socket/StreamInfo Optionals in
  _base/muovi/quattrocento (local-bind in read(); asserts before use) — no
  behaviour change.
- test_docs harness: handle code blocks nested in list items (indented closing
  fence + dedent) so connect-otb-devices.md parses.

ruff + ty clean, 337 tests, docs 0 warnings. Version stays 2.0.2.
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.

1 participant