Data Stream
While acquisition is running, the device sends the neural data as binary UDP datagrams on port 55151, to the PC set with DEST. Without a destination, nothing is sent.
Datagram layout
Every datagram is made of a 12-byte header followed by rows × channels samples. All values are little-endian.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 2 | magic | 0x4D48 — the bytes H, M |
| 2 | 1 | version | Stream format version (currently 1) |
| 3 | 1 | channels | Number of channels in each row |
| 4 | 4 | first_row | Index of the first row of this datagram, counted since the last START |
| 8 | 2 | config_id | Identifier of the current settings; changes whenever a setting changes |
| 10 | 2 | rows | Number of rows in this datagram |
| 12 | rows × channels × 2 | samples | One row per sampling instant: channels unsigned 16-bit values, channel 0 first |
A row is always complete: a datagram never carries half a row. The number of rows per datagram varies with the channel count.
Converting samples to microvolts
Samples are unsigned 16-bit values centred on 32768:
microvolts = (sample − 32768) × 0.195
Detecting lost data
first_row restarts at 0 on every START. Keep the value you expect next:
expected_next = first_row + rows
If a datagram arrives with a larger first_row than expected, the difference is the number of rows lost. If it is smaller (back to 0), a new acquisition has started.
Detecting setting changes
config_id is the same value that INFO and GET_CONFIG report. When it changes during a stream, a setting (sampling rate, filters, channel count…) has changed; read the new values with GET_CONFIG or wait for the matching EVT config event.
Sampling rate
The sampling rate in Hz is the fs field of INFO and GET_CONFIG. It is per channel: the number of rows per second.
Decoding example
import socket
import struct
HEADER = struct.Struct("<HBBIHH") # magic, version, channels, first_row, config_id, rows
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("0.0.0.0", 55151))
expected = None
while True:
data, _ = sock.recvfrom(65535)
magic, version, channels, first_row, config_id, rows = HEADER.unpack_from(data)
if magic != 0x4D48 or len(data) != HEADER.size + rows * channels * 2:
continue # not a stream datagram
if expected is not None and first_row > expected:
print(f"lost {first_row - expected} rows")
expected = first_row + rows
samples = struct.unpack_from(f"<{rows * channels}H", data, HEADER.size)
for r in range(rows):
row = samples[r * channels:(r + 1) * channels]
microvolts = [(v - 32768) * 0.195 for v in row]
# ... use microvolts[0..channels-1]
The destination PC must be set first (DEST <your-ip>). The HMCLS app does this automatically when it connects; close it, or use another PC, when you read the stream from your own script.