synapse-runtime-sdk
The Python SDK for the Synapse runtime: publish and subscribe to topics, serve and call services, keep shared values current with the store, and move H.264 video — all from asyncio.
pip install synapse-runtime-sdk # the SDK
pip install "synapse-runtime-sdk[video]" # + the H.264 encoder/decoder (CPU or NVIDIA)
Linux x86_64, Python ≥ 3.11. A Synapse runtime to connect to: curl -fsSL <release host>/install.sh | bash.
The surface in one screen
from pydantic import BaseModel
from synapse_sdk import Synapse, Topic, Service, Api, ServiceError, SynapseError
class Imu(BaseModel):
accel: tuple[float, float, float]
imu = Topic("imu", schema=Imu) # datagram (default): one datagram per message, newest wins
log = Topic("log", delivery="stream") # ordered, nothing dropped
api = Api(arm=dict(get_pose=Service(input=dict, output=dict))) # api.arm.get_pose.name == "arm/get_pose"
async with Synapse("https://localhost:4470/synapse", "agent", "sim-1") as node: # type agent|observer|web|hmi; the name is minted when absent
pub = await node.publisher(imu) # resolves once the runtime admitted it on the current session
pub.publish(Imu(accel=(0, 0, 9.8))) # synchronous; validated by the schema; dropped+counted while unbound
async for batch in node.subscribe(imu): # one read = the newest message of each publisher (queue_depth=0: every message)
for msg in batch:
print(msg.publisher, msg.data.accel, msg.t_us)
view = node.view(imu, stale_time_ms=200); await view.ready(); print(view.get().data)
reg = await node.serve(api.arm.get_pose, lambda inp, ctx: {"frame": inp["frame"]}) # (input, ctx) -> result; sync runs in a thread, async on the loop
pose = await node.call(api.arm.get_pose, {"frame": "tool0"}, deadline_ms=1000) # await the Run for the result
run = node.call("arm/move_to", {"target": 1}); async for u in run.updates(): ...; await run.cancel()
board = await node.store("mission/board", access="public"); await board.set("phase", "loading"); board.value["phase"] # a value kept current
@node.every(hz=50) # a fixed grid while the node lives (polling bridges)
def poll(): pub.publish(read_imu())
- Identity:
Synapse(url, type, name=None, user_id=None, credential=None).typebounds the role the runtime grants (agent→agent;web/hmi→observeruntil promoted on a project); aweb/hminode withoutuser_idmints one.node.info()hasname,prefix,role,robot,broker_version. - Sessions come and go, the node stays: a lost session is replaced (
reconnect=True, backoffreconnect_initial_ms→reconnect_max_ms), every publisher, registration, subscription and namespace replays its intent on the new session; in-flight calls failconnection_lost; nothing is resumed.node.status/async for s in node.watch_status()/node.on_status(cb).reconnect=Falseends the nodeclosedon a drop. - The runtime decides:
publisher(),serve(),store()resolve on admission; a refusal (already_registered,unauthorized,owner_taken, ...) is asked again, paced, with a warning each time (warn=callback, defaultlogging). - Time:
t_usis the publisher's clock, microseconds.node.clock(),node.time_offsets(),node.to_local_us(t_us, dom),node.age_us(msg);deadline_ms=on a subscription drops a message older than that in our own time domain. - Errors: one
SynapseError(code, message, data, retryable);ServiceError(code, message, data)raised by a handler crosses verbatim (data.appfor an application code); the same error codes in every language. - Media:
Topic(name, content_type="video/h264", fec=True, layout={...})(raw bytes in FEC symbols);VideoPublisher.open(node, topic, encoder),VideoSubscriber(node, topic, decoder, on_image=); see below. - The certificate pin (
broker_cert_sha256=) is discovered when absent (SYNAPSE_BROKER_CERT_SHA256, thenhttp://host:port/certificate.sha256). SIGINT/SIGTERM close every open node, then re-raise (signals=Falseopts out). - Timeouts and periods are
_msintegers everywhere, in every language.
Scene
The active project's world: one set of entities (frames, robots, objects, cameras …) the runtime keeps for the project,
in two planes. The authored plane is what exists — the hierarchy and the components — and every write to it is one
atomic batch the runtime checks. The live plane is what is happening now — poses, joint positions and state of
entities, written many times a second, never saved. What you read is both merged: a live pose replaces xform.t/xform.q
of the entity it names; removing it brings the authored value back.
scene = await node.scene() # the active project's scene; detached (empty) while no project is active
await scene.wait_attached(timeout=10) # until a project's scene is in
world = scene.entities # {id: entity}, read once to build a simulation from it
scene.entity(id); scene.tree(); scene.query(lambda e: e["kind"] == "robot") # the value now; tree() = [{id, children}]
scene.status # state (attached | detached | closed), synced, pending, version, hash, detached_drops
# author: each resolves once scene.entities shows it; a refusal (locked, name_taken, invalid_tree, ...) raises
id = await scene.spawn("box", parent=None, name="crate", xform={"t": [1, 0, 0]}, visual=[{"kind": "box", "size": [1, 1, 1]}])
await scene.patch(id, props={"mass": 2}); await scene.rename(id, "crate-1"); await scene.reparent(id, other, before=sibling)
await scene.set_locked(id, True); await scene.despawn(id, cascade=True) # cascade: with its subtree
async with scene.txn() as t: # one atomic batch when the block ends
a = t.spawn("frame", name="a"); t.patch(a, props={"k": 1})
# live: synchronous, like publish — one call per tick
scene.live(poses=[{"id": arm, "t": [0, 0, 1], "q": [0, 0, 0, 1]}], joints=[{"id": arm, "j": {"shoulder": 0.3}}], lifetime_ms=500)
scene.live(state=[{"id": arm, "s": {"badge": "serving"}}]); scene.live(remove=[arm]) # remove: back to authored
async for ops in scene.watch_changes(): ... # authored changes; also watch_live_removals(), watch_live_denied(), watch_state()
- A live row with
lifetime_msgoes away that long after its last write (timeoutonwatch_live_removals()), and when its writer leaves; one without it stays (a parked robot). The first writer of an id owns its live rows; another writer's rows for it are dropped and reported on itswatch_live_denied(). - While detached, author verbs raise
project_inactiveandlive()drops its batch (counted instatus.detached_drops). - Helpers:
ulid()(a fresh entity id),order_between/order_before/order_after(sibling order values),derived_frame_name(parent, frame).
Derived frames
A robot's robot.frames (names of links) and an entity's hardpoints (named mounting points with a pose) each stand for
frame children of that entity, marked flags.derived. materialize_frames and materialize_hardpoints keep those
children in step with the declaration: they spawn what is missing, rename the frames after the entity is renamed, set a
changed pose, and remove a frame that is no longer declared (refused, as a veto, when something is parented under it).
They only add ops to a txn and return a report of what they did; nothing is sent until the block ends.
from synapse_sdk import materialize_frames, materialize_hardpoints
async with scene.txn() as t: # when a scene is loaded: no poses, so nothing is spawned or moved
report = materialize_frames(t, robot, scene.entities)
async with scene.txn() as t: # after an edit of the robot: the rest poses are your FK at its joints
report = materialize_frames(t, robot, scene.entities, {"tool0": {"t": [0, 0, 1.2], "q": [0, 0, 0, 1]}})
if not report.clean: # raising here sends nothing
raise RuntimeError("; ".join(report.errors + report.vetoes))
async with scene.txn() as t: # hardpoint frames take their pose from the hardpoint itself
materialize_hardpoints(t, entity, scene.entities)
The report: frames (each declared name → its frame's id), spawned, updated, removed, vetoes, errors, and
clean (no vetoes, no errors). A declared frame that does not exist yet and has no pose given is an error, never a spawn.
Examples
Getting started, one verb per file: examples/getting-started/python/01_connect.py … 12_scene.py in the repository.
Video in five lines
from synapse_sdk import Synapse, now_us
from synapse_sdk.media import H264Encoder, H264Decoder, VideoPublisher, VideoSubscriber
enc = H264Encoder(640, 360, 30, bitrate_kbps=2000, backend="auto") # nvenc when a GPU is there, else x264
vp = await VideoPublisher.open(node, "cam/h264", enc) # video/h264 datagram topic with fec + layout; serves `cam/h264/idr`
vp.push(i420_uint8_array, now_us()) # (h*3/2, w) numpy, or (y, u, v) planes; no copy
vs = VideoSubscriber(node, "cam/h264", H264Decoder(), on_image=lambda img: np.asarray(img.y)) # zero-copy planes
Metadata
Release files for synapse-runtime-sdk 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| synapse_runtime_sdk-0.3.0-cp311-abi3-manylinux_2_38_x86_64.whl | CPython 3.11 | abi3 | Linux glibc 2.38+ x86-64 | Details |
Release files / synapse_runtime_sdk-0.3.0-cp311-abi3-manylinux_2_38_x86_64.whl
| Download URL | synapse_runtime_sdk-0.3.0-cp311-abi3-manylinux_2_38_x86_64.whl |
|---|---|
| Size | 9.0 MB |
| Tags | CPython 3.11 Linux glibc 2.38+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
539acb03883f681d91a11d55ccf4194e1ead2a715978c404e05fcf3a88044a46
|
|
BLAKE2b-256 checksum How to use checksums |
8e6896d27bd3676dab13ee4714142e7446b06d1548a4bdb82997f663b0e760ca
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.15
|