Defines (A) the USB link between the device (RP2040) and host (Python app), and (B) the on-disk recording format. Design goals: cross-platform, no custom drivers, robust to byte loss, and fully reconstructable — every state change (Vs, Ri, cal pulse, servo) is timestamped to an exact sample so the original signal can always be recovered in analysis.
- USB CDC (virtual serial) — enumerates as COM/tty, no driver install. Baud is ignored (USB full/high speed); throughput is ample (8 ch × 4 kHz × 3 B ≈ 96 kB/s ≪ USB CDC limit).
- Endianness: little-endian throughout (host + RP2040 are LE).
- Framing: COBS (Consistent Overhead Byte Stuffing) with a
0x00frame delimiter, so a dropped byte costs at most one frame and the stream resyncs on the next delimiter. - Pre-COBS frame:
type(1) | flags(1) | payload(N) | crc16(2)where CRC is CRC-16/CCITT overtype|flags|payload. Receivers drop frames with bad CRC.
| Type | Name | Payload |
|---|---|---|
| 0x01 | INFO | capabilities/identity (A.4) |
| 0x02 | SAMPLES | sample block (A.3) |
| 0x03 | EVENT | timestamped event (A.5) |
| 0x04 | ACK | cmd_type:u8, seq:u8 (command accepted) |
| 0x05 | NACK | cmd_type:u8, seq:u8, err:u8 |
| 0x06 | STATUS | periodic heartbeat: snapshot of per-channel Vs/Ri/servo + flags |
| Type | Name | Payload |
|---|---|---|
| 0x80 | GET_INFO | — |
| 0x81 | CONFIGURE | rate_code:u8, channel_mask:u8 (only when stopped) |
| 0x82 | START | — (begins SAMPLES + EVENT streaming) |
| 0x83 | STOP | — |
| 0x84 | SET_VS | ch:u8, dac_code:u16 |
| 0x85 | SET_RI | ch:u8, mode:u8 (0=1GΩ,1=10TΩ) |
| 0x86 | CAL_PULSE | ch:u8, action:u8 (0=off,1=on), dur_ms:u16 (0=manual) |
| 0x87 | SERVO | ch:u8, mode:u8 (0=off,1=acquire,2=track), target_code:u16, deadband_code:u16, flags:u8 (bit0=freeze) |
| 0x88 | PING | — (keepalive; device replies STATUS) |
Principle: any host command that changes channel state is ACKed and then echoed as an EVENT stamped with the sample_index at which it took effect. So the EVENT log is the single source of truth, regardless of whether a change was host- or servo-initiated.
block_seq : u32
first_sample_index: u64 # monotonic counter since START; master timeline
rate_code : u8
channel_mask : u8 # which of 8 channels are present
n_samples : u16 # rows in this block
status : u8 # bit0 = any channel clipped in this block
data : n_samples × popcount(channel_mask) × int24_le (signed)
int24_le= 3-byte signed little-endian (the native ADS131M08 word). Channels appear in ascending index order per row. Blocks sized to balance latency vs overhead (e.g. 10–50 ms).
format_proto_version, fw_version(maj,min,patch), n_channels, supported_rate_codes[], adc_bits=24, adc_vref_mv, fixed_gain_x100, vs_dac_bits, vs_range_mv, serial[12].
The host uses this to self-configure and to write file calibration (no hard-coded constants).
event_type : u8
channel : u8 # 0xFF = device-wide
sample_index: u64 # exact sample at which it occurred
a, b : i32 # type-specific
text_len : u8 ; text[text_len] # optional (e.g. comments)
| event_type | meaning | a / b |
|---|---|---|
| 1 VS_CHANGE | Vs stepped | a=old_dac, b=new_dac (text=reason: acquire/track/manual) |
| 2 RI_CHANGE | Ri mode switched | a=mode |
| 3 CAL_PULSE | calibration pulse | a=action(0/1), b=amplitude_µV (≈ −50000) |
| 4 CLIP | over-range onset/offset | a=onset(1)/offset(0) |
| 5 SERVO_STATE | servo mode change | a=mode, b=flags |
| 6 COMMENT | host/operator marker | text |
| 7 MODE_MARK | e.g. the manual's "3 pulses = normal, 5 = emf" convention | a=count |
| 8 ERROR | device error | a=code |
- Host opens port →
GET_INFO→ deviceINFO. - Host
CONFIGURE(rate, channel mask); initial per-channelSET_VS/SET_RI/SERVO. - Host
START→ device streamsSAMPLES+EVENT; periodicSTATUS. - Live control via
SET_*/CAL_PULSE/SERVO; each →ACK+ anEVENT. - Host
STOP→ device halts, finalACK.
Clock: sample_index is the master timeline. Absolute time of sample i =
start_time_utc + i / sample_rate. All events reference sample_index, so nothing depends
on host-side timing jitter.
HDF5 is the canonical recording format — self-describing, handles hour-long multichannel
time series with chunking + compression, and is first-class in Python (h5py), MATLAB, R,
Julia. Written incrementally (resizable datasets + periodic flush) so a crash leaves a
valid file up to the last flush.
/ (root attributes)
format_version, device_serial, firmware_version,
start_time_utc (ISO-8601), sample_rate_hz, n_channels, channel_mask,
adc_bits=24, adc_vref_v, fixed_gain, vs_range_mv, vs_dac_bits,
operator, experiment_id, notes
/samples int32 [n_samples, n_active_channels] (24-bit codes)
chunked (e.g. 1 s × channels), gzip or lzf compressed
attrs: channel_ids[], code_to_volts_adc (LSB→V at ADC),
adc_to_input_referred (÷ fixed_gain) # code → input-referred volts
/events compound table (one row per event)
sample_index:u64, time_s:f64, type:enum/str, channel:i8,
a:i32, b:i32, text:vlen-str
# VS_CHANGE, RI_CHANGE, CAL_PULSE, CLIP, SERVO_STATE, COMMENT, MODE_MARK, ERROR
/channels/<id> (optional per-channel metadata group)
attrs: insect_id, species, plant, electrode notes, initial Vs/Ri, ...
Optionally store a convenience /vs_applied decimated trace, but it is always derivable from
/events.
- Code → input-referred volts:
V_in = code × (adc_vref / 2^23) / fixed_gain. - Applied Vs at sample i:
Vs0 + Σ(VS_CHANGE steps with sample_index ≤ i).- As-recorded (servo-corrected) trace =
/samplesdirectly. - Uncompensated trace = add the applied-Vs offset back (input-referred). Because the keep-in-range servo (D11) only steps Vs occasionally and every step is logged, the true continuous signal is always recoverable.
- Display detrend (software high-pass, D11) is a view only — never written into
/samples.
- As-recorded (servo-corrected) trace =
- Calibration pulse: during a CAL_PULSE on→off interval a known ≈ −50 mV is injected at
the input; the step response gives the scale check and the insect-resistance estimate (per
the manual's method). Cal intervals are in
/events.
Write /samples and /events incrementally with periodic flush() (e.g. every 1–2 s). A
power loss leaves a valid HDF5 readable up to the last flush. (Optional: also keep a raw
append-only frame log alongside as a belt-and-suspenders capture that can be re-finalized.)
format_version(file) andformat_proto_version(link, in INFO) are independent and checked on open.- Exports: CSV (samples + events) for spreadsheets; NWB (Neurodata Without Borders, HDF5-based) as a future archival/sharing target to align with the electrophysiology ecosystem.
- Exact rate_code table, block size, STATUS cadence, NACK error codes.
- Whether to compress
/sampleswith gzip (smaller) vs lzf (faster) by default. - Confirm ADS131M08 code scaling (FS, ref) against datasheet at implementation.
- Python reference implementation of the framing + reader (lands in
software/).