Syncbox

Drive and read the Kernel syncbox — 8 channels of digital I/O, analog input, and comparator triggering — for hardware synchronisation alongside your neural recording.

Connecting

The syncbox is reached through the same SdkClient as everything else, on the syncbox namespace. If kortex is installed locally, the no-argument constructor reads its config:

from kernel.sdk import SdkClient

client = SdkClient()
client.syncbox.set_digital_output(0)

To point at a specific kortex — a syncbox-only rig, or a machine without the config file — pass the address with either a secret or a pre-encoded token:

from kernel.sdk import SdkClient

secret = ""  # your organization secret, matching the secret in kortex.json
client = SdkClient("ws://localhost:13254", secret=secret)

The client connects on construction. Pass autostart=False to defer, then call client.start() yourself. Calling start() on an already-connected client is harmless.

Channels & subsystems

The syncbox has 8 channels, numbered 0–7. A channel is configured into one of three subsystems, and that choice determines which telemetry stream its samples appear on:

SubsystemConfigure withSamples appear on
Digital I/Oset_digital_input() / set_digital_output()syncbox/digital
Analog inputset_analog_mode(ch, AnalogMode.Input)syncbox/analog, syncbox/analog_high
Comparatorset_analog_mode(ch, AnalogMode.Comparator)syncbox/comparator

kortex does not range-check these commands. A bad channel index would go straight into the packet header. The SDK validates 0 ≤ channel ≤ 7 for you and raises ValueError otherwise — which is why you should prefer client.syncbox.* over hand-built command dictionaries.

Digital I/O

set_digital_input(channel) set_digital_output(channel) set_digital_output_state(channel, on) set_digital_edge(channel, edge)

Setting a channel's direction and driving it are two separate steps — set_digital_output() only configures the pin.

sb = client.syncbox

sb.set_digital_output(0)                    # channel 0 becomes an output
sb.set_digital_output_state(0, on=True)     # drive it high
sb.set_digital_output_state(0, on=False)    # and low again

Flashing an LED

import time

sb = client.syncbox
sb.set_digital_output(1)

state = False
while True:
    state = not state
    sb.set_digital_output_state(1, on=state)
    time.sleep(1)

Reading inputs

An input channel emits a syncbox/digital sample on whichever edge you select:

from kernel.sdk.syncbox import Edge

sb.set_digital_input(2)
sb.set_digital_edge(2, Edge.Rising)   # or Edge.Falling, or Edge.Both

To poll the current state of all eight channels instead of subscribing:

from kernel.sdk.syncbox import channel_states, channel_directions

state = sb.get_digital_state()
print(state.digital_data)        # int, bit N is the level of channel N
print(state.digital_mask)        # int, bit N is set if channel N is configured

print(channel_states(state))     # [True, False, True, False, False, False, False, False]
print(channel_directions(state))

get_digital_state() returns a DigitalState object holding two 8-bit integers, not a list. Use channel_states() when you want one boolean per channel.

Analog inputs

set_analog_mode(channel, mode) set_hsain_mode(differential=False)

An analog channel either feeds the ADC (samples on syncbox/analog) or the comparator (edges on syncbox/comparator):

from kernel.sdk.syncbox import AnalogMode

sb.set_analog_mode(1, AnalogMode.Input)
sb.set_analog_mode(5, AnalogMode.Comparator)

set_hsain_mode() selects single-ended or differential wiring for the high-speed analog inputs feeding syncbox/analog_high. It is a global setting and takes no channel:

sb.set_hsain_mode()                     # single-ended
sb.set_hsain_mode(differential=True)    # differential

Comparators

set_comparator_threshold(comparator, volts) set_comparator_edge(comparator, edge)

Set a trigger level, then choose which crossing direction emits a sample:

from kernel.sdk.syncbox import Edge

sb.set_analog_mode(5, AnalogMode.Comparator)
sb.set_comparator_threshold(5, 1.1)
sb.set_comparator_edge(5, Edge.Rising)

Versions & state

get_versions() get_digital_state()
v = sb.get_versions()
print(v.version)          # "1.2.3"
print(v.serial_number)
print(v.commit_hash)

Command reference

MethodArgumentsReturns
get_versions()Version
get_digital_state()DigitalState
set_digital_input(channel)0–7ack
set_digital_output(channel)0–7ack
set_digital_output_state(channel, on)0–7, boolack
set_digital_edge(channel, edge)0–7, Edgeack
set_analog_mode(channel, mode)0–7, AnalogModeack
set_hsain_mode(differential)boolack
set_comparator_threshold(comparator, volts)0–7, floatack
set_comparator_edge(comparator, edge)0–7, Edgeack

"ack" means the syncbox acknowledged the command. These calls are synchronous — kortex waits for the device's reply before returning — so a successful return means the hardware applied the setting. They carry no response payload.

Data streams

Syncbox telemetry arrives through the ordinary subscribe() API. Module names may be given short, without the URN prefix.

ModuleFieldsChannelsRate
syncbox/analogid, data8~1 kHz per channel
syncbox/analog_highid, data2~10 kHz per channel
syncbox/analog_outputid, datacontinuous
syncbox/digitaldata, mask8on edge
syncbox/comparatordata, mask8on edge

Callbacks receive a dict keyed by field URN, always including urn:kernel.com/field/timestamp. Analog channels split by index: id 0–7 route to analog, 8 and above to analog_high.

rate means different things here. rate=0 delivers every update as it happens — right for the event-driven digital and comparator streams. A non-zero rate is a batching window in microseconds, so rate=int(10e5) hands you one second of analog samples per callback.

Monitoring example

Subscribe to all three high-traffic streams and report throughput once a second — useful for confirming a syncbox is wired up and keeping pace.

import time
from threading import Lock

from kernel.sdk import SdkClient

TIMESTAMP = "urn:kernel.com/field/timestamp"
DIGITAL_DATA = "urn:kernel.com/field/syncbox/digital/data"

client = SdkClient("ws://localhost:13254", secret=secret)

lock = Lock()
digital, analog, analog_high = [], [], []


def report():
    # 8 analog channels per packet, 2 high-speed channels per packet
    print(f"analog {len(analog) / 8:.0f}/s, high {len(analog_high) / 2:.0f}/s, digital {digital}")
    digital.clear()
    analog.clear()
    analog_high.clear()


def on_digital(data):
    with lock:
        digital.extend(f"{state:08b}" for state in data[DIGITAL_DATA])


def on_analog(data):
    with lock:
        analog.extend(data[TIMESTAMP])


def on_analog_high(data):
    with lock:
        analog_high.extend(data[TIMESTAMP])
        report()


client.subscribe("syncbox/digital", on_digital, rate=0)
client.subscribe("syncbox/analog", on_analog, rate=int(10e5))
client.subscribe("syncbox/analog_high", on_analog_high, rate=int(10e5))

while True:
    time.sleep(1)

Healthy output looks like analog 1000/s, high 10000/s, digital ['00000001']. Rates well below that usually mean the machine is saturated — close other applications, or run kortex with elevated priority.

Enums & helpers

All exported from kernel.sdk.syncbox.

NameMembers / signaturePurpose
AnalogModeInput, ComparatorWhat an analog channel is wired to
EdgeRising, Falling, BothWhich transition emits a sample
channel_states(state)-> list[bool]Decode digital_data into 8 booleans
channel_directions(state)-> list[bool]Decode digital_mask into 8 booleans
NUM_CHANNELS8Channels per subsystem

Edge.Both exists because the underlying firmware spells the two both-edges commands inconsistently — digital uses falling-then-rising, comparator the reverse. The enum hides that.

Errors

RaisedWhen
ValueErrorA channel or comparator index is outside 0–7
TypeErroredge is not an Edge, mode is not an AnalogMode, or a channel is not an int
DisconnectedThe socket is not connected
TimeoutErrorThe syncbox did not respond in time
CommandFailureThe syncbox rejected the command

Everything above except ValueError and TypeError derives from SDKError, so except SDKError catches all device-side failures.