EEG_MI / src /acquisition /README.md
Twu31's picture
Mirror of github.com/twu3202/EEG_MI at 66726e1
d37c079 verified
|
Raw
History Blame Contribute Delete
5.41 kB

Acquisition — cap → LSL bridge + simple 32-ch viewer

Backup in-house acquisition path (see research/Cerelog_adaptation_notes.md and research/OpenBCI_GUI_software_report.md). Everything is LSL-based, so LabRecorder, MNE-LSL, Timeflux, or a forked OpenBCI GUI can also consume the same stream.

[cap] --UDP/TCP--> udp_lsl_bridge.py --LSL 'Cap32'(EEG,32ch) + 'Cap32_Markers'--> viewer.py / LabRecorder

Files

file what
cap_gui.py Main acquisition GUI (direct UDP/TCP): control bar, 32-ch scope (CAR'd + filtered), live spectrum, quality panel, Record.
impedance_gui.py Impedance head-map UI: sends %, shows per-electrode kΩ; falls back to passive contact proxy if the board doesn't inject; restores * on exit.
impedance.py CLI impedance probe (sends %, prints per-ch kΩ + amp@31.2Hz, restores *).
probe.py Socket-level probe (local IPs, init sequence, reframe, per-ch µV + common-mode test).
rt_filter.py RealTimeEEGFilter — vendor-style Butterworth band/low-pass + 50 Hz iir-notch + baseline removal, stateful lfilter for live chunks.
synth.py Synthetic 32-ch EEG so the GUI/pipeline run with no hardware.
udp_lsl_bridge.py Alternative LSL path: cap → LSL 'Cap32' (for LabRecorder/MNE-LSL/Timeflux interop).
viewer.py The LSL-based viewer (pairs with the bridge).

Two acquisition paths: cap_gui.py (self-contained, vendor-style — the main one now) or the LSL bridge + viewer (when you want LabRecorder→XDF or other LSL tools). Both share common/montage.py + the udp_lsl_bridge parser.

Use

conda activate eegmi

# preview the GUI with synthetic data (no hardware) — renders results/cap_gui_preview.png
python src/acquisition/cap_gui.py --screenshot results/cap_gui_preview.png
python src/acquisition/cap_gui.py --source synth                       # live, synthetic
python src/acquisition/cap_gui.py --source udp --host 192.168.4.1 --port 8086   # real cap (UDP)
python src/acquisition/cap_gui.py --source tcp --host 192.168.4.1 --port <tcp_port>  # loss-free

# alternative LSL path (LabRecorder→XDF / Timeflux):
python src/acquisition/udp_lsl_bridge.py --transport udp --host 192.168.4.1 --port 8086
python src/acquisition/viewer.py --source lsl
python src/acquisition/udp_lsl_bridge.py --dry-run                     # parse self-test

Filtering (mirrors the vendor RealTimeEEGFilter)

rt_filter.py: Butterworth order-4 band-pass (lowcut>0) or low-pass (lowcut=0) + optional 50 Hz iirnotch + baseline/drift removal (running-mean subtraction), all via stateful lfilter (zi) for live streaming. GUI defaults 0–40 Hz + notch on; set low=3 for a 3–40 Hz band-pass. The vendor app uses the exact same design (butter + iirnotch + running-mean).

Status (verified on the M5)

  • Bridge parse self-test ✅ (±1000 counts → ±22.35 µV, sign + trigger correct).
  • Viewer renders ✅ (results/cap_gui_preview.png).
  • LSL two-process data flow ✅ (producer → consumer, 32 ch @ 250 Hz).

Protocol — CONFIRMED from the vendor app (fromprovider/main_ui)

Reverse-read from their PyInstaller build (utils.parse_24bit_signed, Controller.change_channels_nums, utils.UDPReceiver/TCPReceiver):

  • FRAME_LEN = n_ch*3 + 9 → 32 ch = 105 B. [0]=0xA0, [1]=seq#(0-255), [2 : 2+3·n_ch] = n_ch × 3-byte big-endian signed samples, then 6 reserved/trigger bytes, [FRAME_LEN-1]=0xC0.
  • µV = signed24 × 0.02235 (if v&0x800000: v-=16777216) — matches our scaling exactly.
  • The bridge's PacketLayout + parse_24bit_signed now mirror this (dry-run verified).

Board commands (CONFIRMED from vendor utils.py/Controller.py — LOWERCASE!)

byte meaning
b'b' start streaming (manual's "B" is WRONG)
b's' stop
b'1'/b'2'/b'3' sample rate 250 / 500 / 1000 Hz
b'*' EEG-wave mode (normal acquisition)
b'%' impedance mode (injects AC current)

Init order that avoids the impedance-mode comb artifact: '*' (EEG mode) → rate → 'b' (start). Our board_init() does exactly this; the UDP socket also mirrors the vendor (bind local port 2244, connect to the board). If you connect and only send b (or uppercase B) without '*', the board stays injecting → every channel shows a regular waveform with an evenly-spaced harmonic comb in the FFT. DC offset is removed in software (running-mean + optional high-pass in rt_filter.py), not on the board.

Two things their code confirms:

  1. TCP is already supported — their TCPReceiver uses TCP_NODELAY. So the loss-free path is available: run our bridge with --transport tcp once you know the TCP port (ask the vendor; their UDP port is 8086). No firmware change may even be needed.
  2. Per-frame sequence counter (byte 1) → our bridge now reports dropped-frame % live and emits lost/N markers (same idea as their check_sequence_continuity).

Still worth asking the vendor: the exact sampling rate (default 250, options 500/1000), the TCP port, whether the 6 reserved bytes carry a real trigger (their UDP build passes trigger=0), and whether the impedance mode (change2Imp/ImpedanceMode + WidgetImpedancePlot exist in their app) actually returns valid readings on this cap.