Skip to content

Building Your Own LSL Implementation

If you’re writing your own program or script and want to publish a Knight Board LSL stream directly — rather than running the standalone app — you only need pylsl on top of your serial connection. The outline below mirrors what knight_lsl_gui.py does internally.

Terminal window
pip install pylsl pyserial numpy

1. Open the Serial Connection and Configure Channels

Section titled “1. Open the Serial Connection and Configure Channels”

Connect at 115200 baud, then send chon_/rldadd_ commands with pauses between them, exactly as described in Command Set.

import serial, time
ser = serial.Serial("COM3", 115200, timeout=1)
for ch in range(1, 9):
ser.write(f"chon_{ch}_12".encode("ascii"))
time.sleep(2)
ser.write(f"rldadd_{ch}".encode("ascii"))
time.sleep(2)

Read and decode the raw serial frames as described in Knight Board Data Format. Watch for the 0xA0 start byte and 0xC0 end byte, and remember that an IMU board sends a longer frame (57 bytes total) than the standard board (21 bytes total) — you can tell them apart by the frame length once packets start arriving.

import numpy as np
NUM_CHANNELS = 8
GAIN = 12
SCALE = 4.0 / (2**15 - 1) / GAIN * 1_000_000.0 # µV per count
def read_eeg_sample(ser):
if ser.read(1) != b"\xA0":
return None
payload = ser.read(20) # counter + 8×EXG + 2 LOFF + end byte
if payload[19] != 0xC0:
return None
sample = np.zeros(NUM_CHANNELS, dtype=np.float64)
for i in range(NUM_CHANNELS):
hi, lo = payload[1 + 2 * i], payload[2 + 2 * i]
raw = (hi << 8) | lo
if raw & 0x8000:
raw -= 0x10000
sample[i] = raw * SCALE
return sample

Describe the stream once — name, type, channel count, sample rate, and per-channel metadata — then push one sample per packet.

from pylsl import StreamInfo, StreamOutlet
info = StreamInfo(
name="NeuroPawnKnight",
type="EEG",
channel_count=NUM_CHANNELS,
nominal_srate=125,
channel_format="float32",
source_id="neuropawn_knight",
)
channels = info.desc().append_child("channels")
for i in range(NUM_CHANNELS):
ch = channels.append_child("channel")
ch.append_child_value("label", f"EXG{i + 1}")
ch.append_child_value("unit", "microvolts")
ch.append_child_value("type", "EEG")
outlet = StreamOutlet(info)
while True:
sample = read_eeg_sample(ser)
if sample is not None:
outlet.push_sample(sample.astype(np.float32).tolist())

Any other script — on the same machine or over the network — can pick up the stream with pylsl’s inlet API:

from pylsl import resolve_streams, StreamInlet
streams = resolve_streams()
inlet = StreamInlet(next(s for s in streams if s.name() == "NeuroPawnKnight"))
while True:
sample, timestamp = inlet.pull_sample()
print(timestamp, sample)