Skip to main content
When the rest of your robot code is Python, you still want its data. The cerulion Python package reads and publishes the same shared-memory topics a graph uses, raw or typed with the ROS 2 message classes, and a node can be written in Python too.
The Python client is built from a Cerulion source checkout: the release archives carry no wheel. It needs Python 3.11 or later, and it reaches topics on the local machine only; it opens no network connection.

Build and install the client

From a checkout of the Cerulion repository, in a virtual environment with Python 3.11 or later:
The wheel is tagged for CPython 3.11 and later, so pip refuses it on an older interpreter.

Read a topic the CLI publishes

Message classes live in cerulion.msgs, one module per ROS 2 package: cerulion.msgs.geometry_msgs.Vector3 is the type a Rust node names as native_ros2_messages::geometry_msgs::Vector3. Pass the class to a subscriber, and each received frame views as an instance of it. Save this as sub_vector3.py and start it:
In a second terminal, publish one message:
The script prints:
frame.view() is a read-only, zero-copy view of the shared-memory slot, and msg.copy() makes an owned copy. Release each frame promptly, and drop its views first: a subscriber can hold only a few frames at once (sub.max_borrowed_samples, 2 by default).
A script binds cerulion.msgs to the workspace that contains the current directory, or to $CERULION_WORKSPACE when it is set, so your workspace’s own message types are classes too. Outside a workspace it sees the built-in messages alone.

Publish typed messages

A schema you define yourself works the same way. This script publishes a Point twice, once from a dict and once by writing a zero-copy loan in place, and reads each back:
publish() of a dict that has missing, extra or ill-typed fields raises cerulion.EncodeError. Every client error derives from cerulion.CerulionError.
One transport serves the whole process: cerulion.connect() returns the same session every time. A Python process that loads this client must not also load another module that embeds the Cerulion runtime.

Read a recording

cerulion.open_bag(path) reads a finalized MCAP bag. topics() lists its channels, and messages(topics=None) yields (topic, record) pairs whose bytes are owned copies; view a record with record.view(Vector3), or record.view("pkg/Name", schemas=s) for a schema of your own.

Write a node in Python

A Python node is a node type like any other: cerulion node build compiles a small library that embeds CPython, and the node’s logic lives in nodes/<type>/node.py.
What to know before you write one:
  • It needs a source checkout. node create --lang python refuses outside one, because the embedding crate is not published to a registry.
  • Exactly one scheduling policy. Give period_ms, one input marked trigger=True, or sync_window_ms for several trigger inputs, so node create --lang python needs -T or --policy. A Python node takes -i and -o repeatedly.
  • Edit node.py, not the scaffold. node modify refuses Python nodes: change node.py and run cerulion node build again.
  • The interpreter is found at build time, in this order: $CERULION_PYTHON, <workspace>/.venv/bin/python, then python3 on PATH. Install the client into that interpreter.
  • One interpreter per process. Python execution is serialized through the GIL; threads your node creates and fork() are not supported. It suits control and integration nodes, not kHz loops: every tick crosses into the interpreter.
  • No state capture, and no Fatal stop. A Python node starts from init on every run, and its errors carry no kind, so it cannot stop itself with NodeError::Fatal.

Next steps

Inspect topics

Find the topics to read, and their types, from the CLI.

Messages and schemas

The built-in message types and how to define your own.