Skip to main content

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.

OffsetSizeFieldDescription
02magic0x4D48 — the bytes H, M
21versionStream format version (currently 1)
31channelsNumber of channels in each row
44first_rowIndex of the first row of this datagram, counted since the last START
82config_idIdentifier of the current settings; changes whenever a setting changes
102rowsNumber of rows in this datagram
12rows × channels × 2samplesOne 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​

read_stream.py
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]
note

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.