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
```bash
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.