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:
| Subsystem | Configure with | Samples appear on |
|---|---|---|
| Digital I/O | set_digital_input() / set_digital_output() | syncbox/digital |
| Analog input | set_analog_mode(ch, AnalogMode.Input) | syncbox/analog, syncbox/analog_high |
| Comparator | set_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
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
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 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
v = sb.get_versions()
print(v.version) # "1.2.3"
print(v.serial_number)
print(v.commit_hash)
Command reference
| Method | Arguments | Returns |
|---|---|---|
get_versions() | — | Version |
get_digital_state() | — | DigitalState |
set_digital_input(channel) | 0–7 | ack |
set_digital_output(channel) | 0–7 | ack |
set_digital_output_state(channel, on) | 0–7, bool | ack |
set_digital_edge(channel, edge) | 0–7, Edge | ack |
set_analog_mode(channel, mode) | 0–7, AnalogMode | ack |
set_hsain_mode(differential) | bool | ack |
set_comparator_threshold(comparator, volts) | 0–7, float | ack |
set_comparator_edge(comparator, edge) | 0–7, Edge | ack |
"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.
| Module | Fields | Channels | Rate |
|---|---|---|---|
syncbox/analog | id, data | 8 | ~1 kHz per channel |
syncbox/analog_high | id, data | 2 | ~10 kHz per channel |
syncbox/analog_output | id, data | — | continuous |
syncbox/digital | data, mask | 8 | on edge |
syncbox/comparator | data, mask | 8 | on 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.
| Name | Members / signature | Purpose |
|---|---|---|
AnalogMode | Input, Comparator | What an analog channel is wired to |
Edge | Rising, Falling, Both | Which 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_CHANNELS | 8 | Channels 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
| Raised | When |
|---|---|
ValueError | A channel or comparator index is outside 0–7 |
TypeError | edge is not an Edge, mode is not an AnalogMode, or a channel is not an int |
Disconnected | The socket is not connected |
TimeoutError | The syncbox did not respond in time |
CommandFailure | The syncbox rejected the command |
Everything above except ValueError and TypeError derives from
SDKError, so except SDKError catches all device-side failures.