Live Streaming to Foxglove
Getting a Mosaico Sequence into Foxglove doesn't require an intermediate file. The Foxglove SDK can open a live WebSocket server directly inside your Python process; anything you log() to it is broadcast immediately to whatever Foxglove app is connected, with nothing ever written to disk. This guide covers replaying an already-ingested Sequence into a live Foxglove session at its original cadence — useful for a review session or a quick sanity check — using RobotJoint data as the running example.
- Python
- C++
- Rust
The C++ SDK is currently in development.
The Rust SDK is currently in development.
pip install mosaicolabs foxglove-sdk
Why a Live WebSocket Sink, Not a File
Writing an MCAP file and opening it in Foxglove is a perfectly valid workflow, but it is a different one: it produces a file you load afterwards. foxglove.start_server() instead opens a WebSocket server that acts as a live sink — every message you log through it is pushed straight to any connected Foxglove client, the moment you log it. Nothing is buffered to disk, and there is no file to clean up afterwards; the data only ever exists as a live stream, exactly like a robot's UI would display it while running. In Foxglove itself, this is "Open connection" → "Foxglove WebSocket" instead of "Open local file".
Locating the Data
Finding and validating the Topic to replay uses the same QuerySequence/QueryTopic + TopicHandler pattern covered in the MuJoCo Visualisation example: filter by RobotJoint.ontology_tag() to find joint-state Topics regardless of how the channel happens to be named, then confirm the match with a TopicHandler before streaming anything.
Mapping Ontology Types onto Well-Known Foxglove Schemas
- Python
- C++
- Rust
The simplest way to get something on screen is foxglove.log(topic, {"some": "dict"}): it creates a schemaless JSON channel, and Foxglove can only render it generically — one line per dictionary key in a Plot panel, at best. The foxglove.channels module ships typed channels for well-known robotics concepts instead, and one of them — JointStatesChannel — maps directly onto RobotJoint: same index-aligned arrays of names, positions, velocities, and efforts, just organized as a list of per-joint structs rather than parallel lists. Using it gets you Foxglove's dedicated joint-state visualizations for free, instead of a generic plot.
from foxglove.channels import JointStatesChannel
from foxglove.messages import JointState, JointStates
# Created ONCE, outside the streaming loop — see "Server Lifecycle" below.
joint_channel = JointStatesChannel("/robot/joint_states")
def to_joint_states(joints: RobotJoint) -> JointStates:
return JointStates(
joints=[
JointState(name=name, position=pos, velocity=vel, effort=eff)
for name, pos, vel, eff in zip(
joints.names, joints.positions, joints.velocities, joints.efforts
)
],
)
The C++ SDK is currently in development.
The Rust SDK is currently in development.
The channel is created once, before the streaming loop, and reused for every message — exactly like a TopicWriter on the write side: creating a new JointStatesChannel for the same topic on every message would just be wasted work.
Streaming Class-less (Unmodeled) Data
- Python
- C++
- Rust
Not every Topic has a class to build a well-known schema from. For a Topic whose message type has no adapter — retrieved via msg.get_data(Unmodeled), as in Advanced: Ingesting Unmodeled Ontologies — there is no JointState-style structure to build. This isn't a workaround: Unmodeled.raw_data is already a plain dict of the same JSON-friendly types (numbers, strings, booleans, nested dicts and lists) that foxglove.log() accepts directly, so you can stream it without ever resolving a Python class for the ontology tag — mirroring how Class-Free Queries let you query the same data without one.
import base64
from mosaicolabs.models.core.unmodeled import Unmodeled
def to_json_safe(value):
# raw_data can contain `bytes` (from a pa.binary() field), which json.dumps
# can't serialize on its own — everything else is already JSON-friendly.
if isinstance(value, bytes):
return base64.b64encode(value).decode("ascii")
if isinstance(value, dict):
return {k: to_json_safe(v) for k, v in value.items()}
if isinstance(value, list):
return [to_json_safe(v) for v in value]
return value
def stream_unmodeled_topic_live(top_handler) -> None:
wall_start = time.monotonic()
for msg in top_handler.get_data_streamer():
relative_ts = (msg.timestamp_ns - top_handler.timestamp_ns_min) / 1.0e9
sleep_for = relative_ts - (time.monotonic() - wall_start)
if sleep_for > 0:
time.sleep(sleep_for)
unmodeled = msg.get_data(Unmodeled)
if unmodeled is None:
continue
foxglove.log(top_handler.name, to_json_safe(unmodeled.raw_data), log_time=msg.timestamp_ns)
The C++ SDK is currently in development.
The Rust SDK is currently in development.
Since there is no schema, Foxglove treats the topic as generic JSON rather than a known message type:
- The Topics panel lists it with no schema name, unlike the typed
/robot/joint_statestopic above. - Add a Raw Messages panel and subscribe to the topic to see
raw_datarendered as a plain, expandable JSON tree — field names match the ontology's Arrow schema exactly (e.g.gyro.x). - A Plot panel can still chart individual numeric fields, using the same dot-notation message path as any other topic (e.g.
/lidar/imu_raw.gyro.x) — you just have to know the path yourself, since there's no schema for Foxglove to autocomplete it from. - There is no dedicated 3D or domain-specific panel: that visualization richness is exactly what a well-known schema like
JointStatesChannelbuys you, and what streaming raw JSON gives up in exchange for needing no adapter or class at all.
The same log_time = msg.timestamp_ns mapping from Two Clocks, Two Timestamps below still applies — it comes from the Message envelope regardless of whether the payload has a class. What doesn't carry over is the schema-level timestamp field: since raw_data's shape is only known at runtime, there's no canonical place to look for a measurement timestamp the way RobotJoint.header.timestamp provides one, even if a particular tag's raw_data happens to contain a similarly-shaped field.
Two Clocks, Two Timestamps
- Python
- C++
- Rust
Channel.log(msg, log_time=...) and the JointStates schema itself both carry a notion of time, and they are not the same clock — the distinction is the same one covered in depth for Message vs Header:
log_timeis the channel-level time Foxglove uses to place a message on its timeline — the playback clock. This is exactlyMessage.timestamp_ns, the envelope timestamp everyRobotJointmessage already carries.JointStates.timestampis the schema's own field for when the joints were actually measured — the sensor clock. This isRobotJoint.header.timestamp, when present.
def to_joint_states(joints: RobotJoint) -> JointStates:
header_ts = joints.header.timestamp if joints.header else None
return JointStates(
timestamp=(
Timestamp(sec=header_ts.seconds, nsec=header_ts.nanoseconds)
if header_ts is not None
else None
),
joints=[
JointState(name=name, position=pos, velocity=vel, effort=eff)
for name, pos, vel, eff in zip(
joints.names, joints.positions, joints.velocities, joints.efforts
)
],
)
# ...
joint_channel.log(to_joint_states(joints), log_time=joint_msg.timestamp_ns)
The C++ SDK is currently in development.
The Rust SDK is currently in development.
Getting these backwards is an easy mistake to make: passing Header.timestamp as log_time would place messages on Foxglove's timeline using each sensor's own clock origin — which, as covered in the Message guide, has no common origin across topics and cannot be relied on for ordering.
Pacing the Replay to Wall-Clock Time
- Python
- C++
- Rust
Streaming a TopicDataStreamer as fast as possible would blast years of data at Foxglove in milliseconds. To replay it at the speed it was recorded, track how far into the recording each message is and sleep until real time catches up — the same pacing technique used in the MuJoCo Visualisation example:
import time
def stream_topic_live(top_handler) -> None:
wall_start = time.monotonic()
for joint_msg in top_handler.get_data_streamer():
relative_ts = (joint_msg.timestamp_ns - top_handler.timestamp_ns_min) / 1.0e9
sleep_for = relative_ts - (time.monotonic() - wall_start)
if sleep_for > 0:
time.sleep(sleep_for)
joints = joint_msg.get_data(RobotJoint)
joint_channel.log(to_joint_states(joints), log_time=joint_msg.timestamp_ns)
The C++ SDK is currently in development.
The Rust SDK is currently in development.
Server Lifecycle
- Python
- C++
- Rust
foxglove.start_server() returns a WebSocketServer handle. It has no context-manager support, so nothing stops it automatically — call .stop() yourself once streaming is done, inside a finally block so a Mosaico error or a Ctrl-C during replay doesn't leave the server dangling:
server = foxglove.start_server()
try:
# ... locate topics and stream, as above ...
pass
finally:
server.stop()
The C++ SDK is currently in development.
The Rust SDK is currently in development.
Full Example
- Python
- C++
- Rust
import time
import foxglove
from foxglove.channels import JointStatesChannel
from foxglove.messages import JointState, JointStates, Timestamp
from mosaicolabs import MosaicoClient, QuerySequence, QueryTopic
from mosaicolabs.models.sensors.robot import RobotJoint
ROBOT_SEQUENCE_NAME = "r2b_robotarm_0"
# Created once, reused for every message.
joint_channel = JointStatesChannel("/robot/joint_states")
def to_joint_states(joints: RobotJoint) -> JointStates:
header_ts = joints.header.timestamp if joints.header else None
return JointStates(
timestamp=(
Timestamp(sec=header_ts.seconds, nsec=header_ts.nanoseconds)
if header_ts is not None
else None
),
joints=[
JointState(name=name, position=pos, velocity=vel, effort=eff)
for name, pos, vel, eff in zip(
joints.names, joints.positions, joints.velocities, joints.efforts
)
],
)
def stream_topic_live(top_handler) -> None:
wall_start = time.monotonic()
for joint_msg in top_handler.get_data_streamer():
relative_ts = (joint_msg.timestamp_ns - top_handler.timestamp_ns_min) / 1.0e9
sleep_for = relative_ts - (time.monotonic() - wall_start)
if sleep_for > 0:
time.sleep(sleep_for)
joints = joint_msg.get_data(RobotJoint)
joint_channel.log(to_joint_states(joints), log_time=joint_msg.timestamp_ns)
def main() -> None:
server = foxglove.start_server()
print(f"Foxglove server listening on ws://localhost:{server.port}")
try:
with MosaicoClient.connect("localhost", 6726) as client:
result = client.query(
QuerySequence().with_name(ROBOT_SEQUENCE_NAME),
QueryTopic().with_ontology_tag(RobotJoint.ontology_tag()),
)
if result is None:
print(f"Sequence '{ROBOT_SEQUENCE_NAME}' not found.")
return
for item in result:
for topic in item.topics:
top_handler = client.topic_handler(item.sequence.name, topic.name)
if top_handler is None:
continue
if top_handler.ontology_tag != RobotJoint.ontology_tag():
continue
stream_topic_live(top_handler)
finally:
server.stop()
if __name__ == "__main__":
main()
The C++ SDK is currently in development.
The Rust SDK is currently in development.
Key Concepts
- A live sink, not a file:
foxglove.start_server()is a WebSocket server your process hosts directly; connected Foxglove clients receive eachlog()call immediately, with nothing written to disk. - Prefer well-known schemas over raw JSON when a class is available:
foxglove.channelsships typed channels (JointStatesChannel,PoseInFrameChannel,LocationFixChannel, and others) that map naturally onto several Mosaico Ontology types and unlock Foxglove's dedicated panels, instead of a generic Plot fed by ad-hoc dictionaries. - When there's no class,
raw_datais already JSON-shaped:Unmodeled.raw_dataneeds no schema mapping at all — only abytes-to-base64 conversion — before it can be logged as generic JSON, at the cost of losing Foxglove's dedicated panels for that topic. log_timeisMessage.timestamp_ns; the schema's own timestamp isHeader.timestamp: the same envelope/measurement distinction covered for the SDK's ownMessageclass applies identically on the Foxglove side.- The channel is stateful, create it once: like a
TopicWriter, aJointStatesChannelshould be created once per topic and reused for every message, not recreated in the streaming loop. - This same pattern extends to truly live data: nothing here is specific to replaying an archived Sequence — a process ingesting live robot data into Mosaico (as in the Writing guides) can log to a Foxglove channel side by side with pushing to a
TopicWriter, mirroring the live stream to Foxglove as it is being recorded. - Scrubbing and playback control are possible but out of scope here: the WebSocket server supports advertising
Capability.PlaybackControland handling seek/pause requests via aServerListener, which would let a Foxglove client scrub through the Sequence instead of only watching it play forward. That is a natural next step once the streaming pattern above is in place.