The SynapseDevice gRPC service defines the control plane for a Synapse device. Clients use these RPCs to inspect and operate a device. Data produced by a running signal chain is exposed separately through Taps.
See the SynapseDevice service definition for the canonical request and response types.
| RPC | Request/response pattern | Purpose |
|---|---|---|
Info | Unary | Return device identity, state, peripherals, storage, and the active configuration. |
Configure | Unary | Validate and install a signal-chain configuration. |
Start | Unary | Start the configured signal chain. |
Stop | Unary | Stop the running signal chain. |
Query | Unary | Run a diagnostic or metadata query and return one response. |
StreamQuery | Server streaming | Run a diagnostic query that returns a sequence of responses. |
DeployApp | Bidirectional streaming | Upload and install a packaged Synapse App. |
ListApps | Unary | List installed Synapse Apps and their versions. |
ListFiles | Unary | List files exposed by the device. |
WriteFile | Unary | Write a file to the device. |
ReadFile | Server streaming | Download a file in chunks. |
DeleteFile | Unary | Delete a file from the device. |
GetLogs | Unary | Return historical logs matching a time and severity filter. |
TailLogs | Server streaming | Stream new log entries as they are produced. |
UpdateDeviceSettings | Unary | Validate, persist, and apply device-defined settings. |
Info
Info returns the current identity and state of a Synapse device.
rpc Info(google.protobuf.Empty) returns (DeviceInfo)Allowed device state: Any. Info does not change the state.
Response
| JSON field | Type | Rules |
|---|---|---|
name | string | singular |
serial | string | singular |
synapseVersionprotobuf: synapse_version | uint32 | singular |
firmwareVersionprotobuf: firmware_version | uint32 | singular |
status | Status | singular |
peripherals | Peripheral | repeated |
configuration | DeviceConfiguration | singular |
The headstage implementation returns kOk for a healthy device. If the device is in its error state, the response reports kUndefinedError and includes the message Device in error state.
import synapse as syn
device = syn.Device("192.168.42.1")
info = device.info()
if info is None:
raise RuntimeError("Unable to read device information")
print(info.name)
print(info.status.state)
print(info.peripherals)Configure
Configure validates and installs a signal chain on the device.
rpc Configure(DeviceConfiguration) returns (Status)Allowed device state: kStopped. The device transitions through kInitializing and returns to kStopped. Other states return kFailedPrecondition.
Request
| JSON field | Type | Rules |
|---|---|---|
nodes | NodeConfig | repeated |
connections | NodeConnection | repeated |
Response
| JSON field | Type | Rules | Description |
|---|---|---|---|
message | string | singular | — |
code | StatusCode | singular | — |
state | DeviceState | singular | — |
power | DevicePower | singular | — |
storage | DeviceStorage | singular | — |
signalChainprotobuf: signal_chain | SignalChainStatus | singular | — |
timeSyncPortprotobuf: time_sync_port | uint32 | singular | If the device supports a time sync server (see time.proto) Then the UDP echo server will be available at this port |
A DeviceConfiguration contains:
nodes: the configuration for every node in the signal chainconnections: directed connections from each source node ID to a destination node ID
See JSON Configuration for instructions and complete examples.
The device validates node support, peripheral IDs, node parameters, connection compatibility, and available resources before accepting the configuration. You can inspect the returned Status.code and Status.message to determine whether configuration succeeded.
import synapse as syn
channels = [
syn.Channel(id=channel_id, electrode_id=channel_id, reference_id=520)
for channel_id in range(32)
]
broadband = syn.BroadbandSource(
peripheral_id=1000,
bit_width=12,
sample_rate_hz=10000,
gain=1.0,
signal=syn.SignalConfig(
electrode=syn.ElectrodeConfig(
channels=channels,
low_cutoff_hz=57.0,
high_cutoff_hz=4365.0,
)
),
)
config = syn.Config()
config.add_node(broadband)
device = syn.Device("192.168.42.1")
status = device.configure_with_status(config)
if status is None or status.code != 0:
raise RuntimeError(status.message if status else "Configure RPC failed")Start
Start starts the signal chain previously installed by Configure. See Device Lifecycle for the complete state model.
rpc Start(google.protobuf.Empty) returns (Status)Allowed device state: kStopped with a non-empty configured signal chain. Success transitions the device to kRunning.
Response
| JSON field | Type | Rules | Description |
|---|---|---|---|
message | string | singular | — |
code | StatusCode | singular | — |
state | DeviceState | singular | — |
power | DevicePower | singular | — |
storage | DeviceStorage | singular | — |
signalChainprotobuf: signal_chain | SignalChainStatus | singular | — |
timeSyncPortprotobuf: time_sync_port | uint32 | singular | If the device supports a time sync server (see time.proto) Then the UDP echo server will be available at this port |
The headstage implementation returns:
kOkwhen the device starts.kInvalidConfigurationwhen the device has not been configured.kFailedPreconditionwhen the device is already running or a configured peripheral cannot be found.kInternalErrorwhen the device cannot start.
import synapse as syn
device = syn.Device("192.168.42.1")
status = device.start_with_status()
if status is None or status.code != 0:
raise RuntimeError(status.message if status else "Start RPC failed")Stop
Stop stops the running signal chain. See Device Lifecycle for the complete state model.
rpc Stop(google.protobuf.Empty) returns (Status)Allowed device state: kRunning. Success transitions the device to kStopped; every other state returns kFailedPrecondition.
Response
| JSON field | Type | Rules | Description |
|---|---|---|---|
message | string | singular | — |
code | StatusCode | singular | — |
state | DeviceState | singular | — |
power | DevicePower | singular | — |
storage | DeviceStorage | singular | — |
signalChainprotobuf: signal_chain | SignalChainStatus | singular | — |
timeSyncPortprotobuf: time_sync_port | uint32 | singular | If the device supports a time sync server (see time.proto) Then the UDP echo server will be available at this port |
The headstage implementation returns kOk when the device stops, kFailedPrecondition when the device is not running, and kInternalError when the device cannot stop.
import synapse as syn
device = syn.Device("192.168.42.1")
status = device.stop_with_status()
if status is None or status.code != 0:
raise RuntimeError(status.message if status else "Stop RPC failed")Query
Query runs a diagnostic or metadata operation and returns one response.
rpc Query(QueryRequest) returns (QueryResponse)Allowed device state: Any. Query does not change the state. A peripheral query can fail if that peripheral is in use.
Request
| JSON field | Type | Rules |
|---|---|---|
queryTypeprotobuf: query_type | QueryType | singular |
impedanceQueryprotobuf: impedance_query | ImpedanceQuery | oneof query |
sampleQueryprotobuf: sample_query | SampleQuery | oneof query |
selfTestQueryprotobuf: self_test_query | SelfTestQuery | oneof query |
listTapsQueryprotobuf: list_taps_query | ListTapsQuery | oneof query |
getSettingsQueryprotobuf: get_settings_query | GetSettingsQuery | oneof query |
Response
| JSON field | Type | Rules |
|---|---|---|
status | Status | singular |
data | uint32 | repeated |
impedanceResponseprotobuf: impedance_response | ImpedanceResponse | oneof response |
selfTestResponseprotobuf: self_test_response | SelfTestResponse | oneof response |
listTapsResponseprotobuf: list_taps_response | ListTapsResponse | oneof response |
getSettingsResponseprotobuf: get_settings_response | GetSettingsResponse | oneof response |
Only one Query or StreamQuery can execute at a time.
The headstage implementation handles:
impedance_query: measure the magnitude and phase for selected electrodes on a peripheral.self_test_query: run the self-test exposed by a peripheral.list_taps_query: list the Taps currently exposed by the configured signal chain.get_settings_query: return current device settings and the device-defined settings schema.
The response contains a Status and the response message corresponding to the selected query. A concurrent Query or StreamQuery returns gRPC RESOURCE_EXHAUSTED.
import synapse as syn
from synapse.api.query_pb2 import QueryRequest
device = syn.Device("192.168.42.1")
request = QueryRequest(
query_type=QueryRequest.kListTaps,
list_taps_query={},
)
response = device.query(request)
if response is None or response.status.code != 0:
raise RuntimeError(response.status.message if response else "Query RPC failed")
for tap in response.list_taps_response.taps:
print(tap)Read device settings
Settings are read through Query using get_settings_query.
The resulting GetSettingsResponse contains the current DeviceSettings.values and a schema describing every accepted key, value type, default, description, and allowed value.
import synapse as syn
from synapse.client import settings
device = syn.Device("192.168.42.1")
print(settings.get_available_settings(device))
print(settings.get_all_settings(device))StreamQuery
StreamQuery runs a diagnostic operation and streams results as they become available.
rpc StreamQuery(StreamQueryRequest) returns (stream StreamQueryResponse)Allowed device state: Any. StreamQuery does not change the state. A peripheral query can fail if that peripheral is in use.
Response stream
| JSON field | Type | Rules |
|---|---|---|
code | StatusCode | singular |
message | string | singular |
timestampNsprotobuf: timestamp_ns | uint64 | singular |
impedance | ImpedanceResponse | oneof response |
selfTestprotobuf: self_test | SelfTestResponse | oneof response |
Only one Query or StreamQuery can execute at a time.
The headstage implementation supports streaming impedance and self-test queries. Each response includes a status code, message, timestamp, and the impedance or self-test result. Unsupported query types return gRPC INVALID_ARGUMENT.
import synapse as syn
from synapse.api.query_pb2 import QueryRequest, StreamQueryRequest
device = syn.Device("192.168.42.1")
request = StreamQueryRequest(
request=QueryRequest(
query_type=QueryRequest.kImpedance,
impedance_query={
"peripheral_id": 100,
"electrode_ids": [0, 1, 2, 3],
},
)
)
for response in device.stream_query(request):
if response.code != 0:
raise RuntimeError(response.message)
for measurement in response.impedance.measurements:
print(measurement.electrode_id, measurement.magnitude, measurement.phase)DeployApp
DeployApp uploads and installs a packaged Synapse App.
rpc DeployApp(stream AppPackageChunk) returns (stream AppDeployResponse)Allowed device state: Any. DeployApp does not change the state.
Request stream
| JSON field | Type | Rules |
|---|---|---|
metadata | PackageMetadata | oneof data |
fileChunkprotobuf: file_chunk | bytes | oneof data |
Response stream
| JSON field | Type | Rules | Description |
|---|---|---|---|
status | StatusCode | singular | — |
message | string | singular | Informational messages |
The client first sends PackageMetadata, including the package name, version, size, and SHA-256 checksum, followed by chunks of package data. The device streams informational and status responses while it validates and installs the package. A failed installation returns a gRPC INTERNAL error.
The Python client's deployment helper calculates the package metadata, streams the package, and processes the deployment responses:
from synapse.cli.deploy import deploy_package
deployed = deploy_package(
"192.168.42.1",
"/path/to/synapse-example-app_0.1.0_arm64.deb",
)
if not deployed:
raise RuntimeError("DeployApp RPC failed")ListApps
ListApps returns the Synapse Apps installed on the device.
rpc ListApps(ListAppsRequest) returns (ListAppsResponse)Allowed device state: Any. ListApps does not change the state.
Each AppInfo in the response contains the installed app's name and version. ListAppsRequest has no fields.
import synapse as syn
device = syn.Device("192.168.42.1")
response = device.list_apps()
if response is None:
raise RuntimeError("ListApps RPC failed")
for app in response.apps:
print(app.name, app.version)ListFiles
ListFiles returns the files exposed by the device. See Filesystem for the device filesystem mental model.
rpc ListFiles(google.protobuf.Empty) returns (ListFilesResponse)Each file entry contains its name, size, creation and modification timestamps, and type.
The Python client exposes this RPC through the generated stub:
import synapse as syn
from google.protobuf.empty_pb2 import Empty
device = syn.Device("192.168.42.1")
response = device.rpc.ListFiles(Empty())
for file in response.files:
print(file.name, file.size, file.type)WriteFile
WriteFile writes the supplied bytes to a named file on the device. See Filesystem for the device filesystem mental model, reserved folders, and SciFi 2 USB access.
rpc WriteFile(WriteFileRequest) returns (WriteFileResponse)Response
| JSON field | Type | Rules |
|---|---|---|
name | string | singular |
bytesWrittenprotobuf: bytes_written | uint64 | singular |
The request contains name and data. The response returns the file name and number of bytes written.
import synapse as syn
from synapse.api.files_pb2 import WriteFileRequest
device = syn.Device("192.168.42.1")
response = device.rpc.WriteFile(
WriteFileRequest(name="/data/example.bin", data=b"example data")
)
print(response.name, response.bytes_written)ReadFile
ReadFile downloads a named file as a server stream. See Filesystem for the device filesystem mental model, reserved folders, and SciFi 2 USB access.
rpc ReadFile(ReadFileRequest) returns (stream ReadFileResponse)Response stream
| JSON field | Type | Rules |
|---|---|---|
name | string | singular |
data | bytes | singular |
startOffsetprotobuf: start_offset | uint32 | singular |
fileTotalLengthprotobuf: file_total_length | uint32 | singular |
Each response contains a chunk of file data, its starting offset, and the total file length. Use the offset and total length to assemble and verify the downloaded file.
import synapse as syn
from synapse.api.files_pb2 import ReadFileRequest
device = syn.Device("192.168.42.1")
with open("example.bin", "wb") as output:
for response in device.rpc.ReadFile(
ReadFileRequest(name="/data/example.bin")
):
output.seek(response.start_offset)
output.write(response.data)DeleteFile
DeleteFile deletes a named file from the device. See Filesystem for the device filesystem mental model, reserved folders, and SciFi 2 USB access.
rpc DeleteFile(DeleteFileRequest) returns (DeleteFileResponse)Response
| JSON field | Type | Rules |
|---|---|---|
name | string | singular |
statusCodeprotobuf: status_code | StatusCode | singular |
The response identifies the file and reports the resulting StatusCode.
import synapse as syn
from synapse.api.files_pb2 import DeleteFileRequest
from synapse.api.status_pb2 import StatusCode
device = syn.Device("192.168.42.1")
response = device.rpc.DeleteFile(
DeleteFileRequest(name="/data/example.bin")
)
if response.status_code != StatusCode.kOk:
raise RuntimeError(f"DeleteFile failed: {response.status_code}")GetLogs
GetLogs returns retained log entries matching a time range and minimum severity.
rpc GetLogs(LogQueryRequest) returns (LogQueryResponse)Allowed device state: Any state. GetLogs does not change the state.
Request
| JSON field | Type | Rules | Description |
|---|---|---|---|
startTimeNsprotobuf: start_time_ns | uint64 | singular | Optional start time, inclusive |
endTimeNsprotobuf: end_time_ns | uint64 | singular | Optional end time, exclusive |
sinceMsprotobuf: since_ms | uint64 | singular | Optional time since current time, in milliseconds Will ignore the start and end time |
minLevelprotobuf: min_level | LogLevel | singular | Optional minimum level, defaults to INFO |
Set either:
since_msto request entries from a recent interval, orstart_time_nsandend_time_nsto request an explicit time range.
When since_ms is set, the headstage ignores the explicit start and end times. If min_level is unspecified, the headstage defaults to LOG_LEVEL_INFO. Each returned entry includes its timestamp, severity, source, and message.
import synapse as syn
device = syn.Device("192.168.42.1")
response = device.get_logs(log_level="WARNING", since_ms=60_000)
if response is None:
raise RuntimeError("GetLogs RPC failed")
for entry in response.entries:
print(entry.timestamp_ns, entry.level, entry.source, entry.message)TailLogs
TailLogs streams new log entries at or above a minimum severity.
rpc TailLogs(TailLogsRequest) returns (stream LogEntry)Allowed device state: Any state. TailLogs does not change the state.
The stream remains active until the client cancels it or the server's stream duration limit is reached. The headstage limits the number of simultaneous log subscribers and returns gRPC RESOURCE_EXHAUSTED when no subscription slot is available.
import synapse as syn
device = syn.Device("192.168.42.1")
for entry in device.tail_logs(log_level="INFO"):
print(entry.timestamp_ns, entry.level, entry.source, entry.message)UpdateDeviceSettings
UpdateDeviceSettings validates, persists, and applies one or more settings. See the Settings concept for schema behavior. Device-specific settings are listed in the Product Details for SciFi and SciFi 2.
rpc UpdateDeviceSettings(UpdateDeviceSettingsRequest)
returns (UpdateDeviceSettingsResponse)Allowed device state: kStopped. Every other state returns gRPC FAILED_PRECONDITION. The update does not change the state.
Request
| JSON field | Type | Rules | Description |
|---|---|---|---|
settings | DeviceSettings | singular | Only keys present in settings.values will be updated. |
Response
| JSON field | Type | Rules |
|---|---|---|
status | Status | singular |
updatedSettingsprotobuf: updated_settings | DeviceSettings | singular |
Only keys present in settings.values are updated. The response includes a Status and the resulting settings. The accepted keys are device-defined; clients should read the schema before constructing an update.
Current SciFi settings include the user-facing device name, timestamp source, FPGA clock frequency, and per-network WiFi MAC address policy.
import synapse as syn
from synapse.client import settings
device = syn.Device("192.168.42.1")
device.stop()
updated_name = settings.set_setting(device, "name", "recording-room-a")
print(updated_name)