When a Stream Out or Stream In node is configured within a Synapse device, it opens a UDP socket to send or receive data, respectively. The corresponding NodeSocket
is reported with every Status
reply message from the control plane API as well as the DeviceInfo
message. You can use the same Stream Out node instance you created when configuring the device to then read data from the socket, via its read
method. The same is true for Stream In, via its write
method.
Connect
Configuring a device to stream data out or in first requires connecting to the device.
import synapse as syn
# Connect to a device
device = syn.Device(uri)
# Get the device's info
info = device.info()
assert info is not None, "Couldn't get device info"
print(f"Device info: {info}")
Stream Out
Next, you'll need to configure the recording parameters on the device, add nodes to record and stream out, connect those nodes, and then start the device.
import synapse as syn
device = syn.Device("127.0.0.1:647")
info = device.info()
print("Device info: ", device.info())
stream_out = syn.StreamOut(label="my broadband", multicast_group="239.0.0.1")
e_broadband = syn.ElectricalBroadband(
peripheral_id=2,
channels=[syn.Channel(id=c, electrode_id=c * 2, reference_id=c * 2 + 1) for c in range(32)],
sample_rate=30000,
bit_width=12,
gain=20.0,
low_cutoff_hz=500.0,
high_cutoff_hz=6000.0,
)
config = syn.Config()
config.add_node(stream_out)
config.add_node(e_broadband)
config.connect(e_broadband, stream_out)
device.configure(config)
# Start the device
assert device.start()
# Receive data
try:
while True:
# The data type will depend on the configuration of the device -- on the node connected to the StreamOut node
data: ElectricalBroadbandData = stream_out.read()
print(f"timestamp 0: {t0}")
print(f"n channels: {len(data.channels)}")
for c in data.channels:
print(f" - [{c.channel_id}]: {c.channel_data}")
# Stop the device
assert device.stop()
Stream In
import random
import time
import synapse as syn
# Configure the device
o_stim = syn.OpticalStimulation()
stream_out = syn.StreamIn()
config = syn.Config()
config.add_node(stream_in)
config.add_node(o_stim)
config.connect(stream_in, o_stim)
device.configure(config)
# Start the device
assert device.start()
# Send data
try:
while True:
payload = SpiketrainData(
t0=time.time_ns() / 1000.0,
spike_counts=[random.randint(0, 3) for _ in range(100)]
)
stream_in.write(payload)
# Stop the device
assert device.stop()
Neural Data Transport Protocol
The Synapse protocol uses Neural Data Transport Protocol (NDTP) to structure the data packets for transmission between nodes over UDP. NDTP structures the message into a 96-bit header, a payload that varies in size according to the originating node, and a 16-bit crc. The Steam Out node handles the packing and unpacking of messages for you.
Struct | Message | ||
Field | .header | .payload | .crc16 |
Width (bits) | 96 | variable | 16 |
Width (bytes) | 12 | variable | 2 |
Example of a Broadband Payload
Struct | Payload | Repeated Channel Data | |||||||
Field | .signed | .bit_width | ch_count | sample_rate | .ch_id | .sample_count | .ch_data | ... | .ch_data |
Width (bits) | 1 | 7 | 24 | 16 | 24 | 16 | bit_width | ... | bit_width |
Width (bytes) | 1 | 3 | 2 | 3 | 2 | bit_width / 8 | ... | bit_width |