Creating a Custom Data Parser
The Knight Board sends one fixed-length binary frame per sample over its USB serial connection. A custom parser only needs to synchronize on the frame markers, decode the common EEG block, and use the correct frame length for the firmware mode.
Serial Settings
Section titled “Serial Settings”| Setting | Value |
|---|---|
| Baud rate | 115200 |
| Data format | 8 data bits, no parity, 1 stop bit |
| Sample rate | 125 frames per second |
| Start byte | 0xA0 |
| End byte | 0xC0 |
Firmware may print human-readable status lines before streaming starts. A
parser should discard input until it finds a complete frame beginning with
0xA0 and ending with 0xC0. Custom firmware can suppress the boot messages by
calling neuropawn.quiet(true) before neuropawn.setup().
Common EEG Block
Section titled “Common EEG Block”The first 20 bytes are identical in both modes:
| Offset | Size | Field |
|---|---|---|
0 | 1 | Start of frame (0xA0) |
1 | 1 | Frame counter, rolling from 0 through 255 |
2 | 16 | Eight signed 16-bit EEG samples, big-endian, channels 1 to 8 |
18 | 1 | Positive-input electrode status, bit 0 = channel 1 |
19 | 1 | Negative-input electrode status, bit 0 = channel 1 |
EEG samples are signed two’s-complement values. In Python, decode all eight at
once with struct.unpack_from(">8h", frame, 2).
EEG-Only Format (NP_DEFAULT)
Section titled “EEG-Only Format (NP_DEFAULT)”An EEG-only frame is 21 bytes:
| Offset | Size | Field |
|---|---|---|
0-19 | 20 | Common EEG block |
20 | 1 | End of frame (0xC0) |
No IMU placeholder bytes are present. The end marker immediately follows the two electrode-status bytes.
IMU Format (NP_IMU)
Section titled “IMU Format (NP_IMU)”An IMU frame is 57 bytes. It contains the same 20-byte EEG block followed by
nine little-endian IEEE-754 float32 values:
| Offset | Size | Field |
|---|---|---|
0-19 | 20 | Common EEG block |
20-31 | 12 | Accelerometer X, Y, Z in m/s2 |
32-43 | 12 | Gyroscope X, Y, Z in rad/s |
44-55 | 12 | Magnetometer X, Y, Z in microtesla |
56 | 1 | End of frame (0xC0) |
Use struct.unpack_from("<9f", frame, 20) to decode the complete IMU payload.
If the firmware is in NP_IMU mode but the sensor is absent, all nine values
are zero and the frame remains 57 bytes long.
Reference Python Parser
Section titled “Reference Python Parser”Install pySerial with pip install pyserial,
then select the mode that matches the firmware:
import structfrom dataclasses import dataclass
import serial
START_BYTE = 0xA0END_BYTE = 0xC0FRAME_LENGTHS = {"default": 21, "imu": 57}
@dataclassclass KnightSample: counter: int eeg_counts: tuple[int, ...] positive_status: int negative_status: int acceleration: tuple[float, float, float] | None = None gyroscope: tuple[float, float, float] | None = None magnetometer: tuple[float, float, float] | None = None
class KnightParser: def __init__(self, mode: str = "default"): if mode not in FRAME_LENGTHS: raise ValueError("mode must be 'default' or 'imu'") self.mode = mode self.frame_length = FRAME_LENGTHS[mode] self.buffer = bytearray()
def feed(self, data: bytes): """Yield every complete sample found in an arbitrary serial chunk.""" self.buffer.extend(data)
while True: start = self.buffer.find(bytes([START_BYTE])) if start < 0: self.buffer.clear() return if start: del self.buffer[:start] if len(self.buffer) < self.frame_length: return
if self.buffer[self.frame_length - 1] != END_BYTE: del self.buffer[0] continue
frame = bytes(self.buffer[: self.frame_length]) del self.buffer[: self.frame_length] yield self.decode(frame)
def decode(self, frame: bytes) -> KnightSample: eeg = struct.unpack_from(">8h", frame, 2) sample = KnightSample( counter=frame[1], eeg_counts=eeg, positive_status=frame[18], negative_status=frame[19], )
if self.mode == "imu": imu = struct.unpack_from("<9f", frame, 20) sample.acceleration = imu[0:3] sample.gyroscope = imu[3:6] sample.magnetometer = imu[6:9]
return sample
port = serial.Serial("COM3", 115200, timeout=1)parser = KnightParser(mode="imu")
while True: for sample in parser.feed(port.read(port.in_waiting or 1)): print(sample.counter, sample.eeg_counts, sample.acceleration)The parser accepts arbitrary chunks rather than assuming that one serial read equals one frame. On a bad end marker it discards the candidate start byte and searches again, allowing it to recover from boot text, dropped bytes, or opening the port in the middle of a stream.
Detecting Dropped Frames
Section titled “Detecting Dropped Frames”For consecutive frames, the expected counter is:
expected = (previous_counter + 1) & 0xFFif sample.counter != expected: dropped = (sample.counter - expected) & 0xFF print(f"Dropped {dropped} frame(s)")Do not use the frame counter as a timestamp; it wraps every 256 samples. Record host arrival times separately if your application needs timing metadata.
Converting EEG Counts
Section titled “Converting EEG Counts”The EEG scale depends on the configured gain:
gain = 12scale = 4 / (2**15 - 1) / gain * 1_000_000eeg_microvolts = tuple(value * scale for value in sample.eeg_counts)Keep the parser’s gain synchronized with the chon_<channel>_<gain> commands
sent to the board. For applications that do not need a custom transport,
BrainFlow already handles framing, conversion, and its
ring buffer.
