Onboard Pi, camera and RViz

Onboard Pi, camera and RViz

Run the Marty driver on an onboard Raspberry Pi and use another Linux computer for RViz. The Pi talks to Marty's RIC controller through the expansion UART cable. The Pi camera sends encoded video to the workstation; a separate receiver there publishes a standard ROS image topic. No Axiom or dashboard is required.

Architecture

Marty UART and ROS telemetry run separately from the Pi camera video stream

Where Process Connection / job
Pi marty_driver in a headless ROS 2 Jazzy Docker container Owns the expansion UART connection to RIC and provides Marty telemetry, services and motion action over ROS DDS.
Pi Native rpicam-vid Captures the camera and sends H.264 in an MPEG-TS stream over TCP. It is not a ROS publisher.
Linux workstation marty_camera receiver Decodes the stream with OpenCV/FFmpeg and publishes /marty/camera/image_raw.
Linux workstation RViz Image display Subscribes to the ROS image topic. It does not connect directly to the camera or UART.

ROS telemetry/control and the TCP video stream are independent network paths. A working image does not prove that the Marty driver is connected; a working IMU does not prove video is arriving.

Requirements

  • Marty V2, the Robotical expansion cable, a compatible onboard Pi and Pi camera.
  • 64-bit Raspberry Pi OS on the Pi; the tested setup used a Pi 3B+ with Bookworm and an IMX708 camera. Other models/camera combinations need their own checks.
  • Ubuntu 24.04 and ROS 2 Jazzy on the workstation, with a graphical desktop for RViz. The driver container supplies Jazzy on the Pi; native ROS installation on Raspberry Pi OS is not required by this workflow.
  • Both machines on a network that permits ROS DDS discovery/traffic and TCP access from the workstation to the Pi's camera port (default 5000). Use the same ROS domain on both.

Follow the Raspberry Pi mounting, cable and UART guide for wiring and board-specific UART configuration. Its direct Python examples are a separate workflow; do not run another MartyPy process on the same UART while the ROS driver owns it. Verify the serial device for your Pi model rather than assuming every Pi exposes the same port.

Prepare the Pi

All commands in this section run on the Pi, from Bash. The camera must be visible to the native camera tools:

# Confirm 64-bit userspace: this should print arm64.
dpkg --print-architecture
# List cameras detected by the native Raspberry Pi camera stack.
rpicam-hello --list-cameras
# Check the configured expansion UART exists (example for the tested Pi setup).
ls -l /dev/ttyAMA0
# Install the checkout and container tools.
sudo apt update
sudo apt install git docker.io
# Download the current Marty ROS source into a chosen directory.
git clone https://github.com/robotical/marty-ros2.git
cd marty-ros2
# Build the headless Jazzy driver image; this may take time on a small Pi.
sudo docker build -f examples/onboard-pi/Dockerfile -t marty-driver:jazzy .

For an existing checkout, update with git pull --ff-only instead of cloning again. The image contains the driver, interfaces and bringup packages, with MartyPy installed inside it. Its build uses one compiler job for the Pi's limited memory. The scripts use sudo docker; Docker group membership is not required.

Create/edit the Pi's ignored local config/robots.yaml with this example, replacing the serial path if your hardware differs:

martys:
  marty:
    method: exp
    locator: /dev/ttyAMA0
    serial_baud: 921600
    auto_connect: false
    auto_reconnect: false
    subscribe_rate_hz: 10.0

Here exp selects Marty's expansion UART connection. This is separate from a USB connection's settings. Use the expansion interface's rate, not a USB example's baud rate.

Set the Pi's config/machine.env to:

# Use the same domain as the workstation; 0 is an example.
ROS_DOMAIN_ID=0
# Set this and robots.yaml to the same actual UART device.
MARTY_SERIAL_PORT=/dev/ttyAMA0
# Resolve the robot configuration relative to this checkout.
MARTY_ROS_ROBOTS_FILE="$MARTY_ROS_ROOT/config/robots.yaml"

These Pi scripts load configuration without sourcing a native ROS installation. The usual workstation virtual environment is not needed on the Pi host.

Start the Pi driver and video

From the Pi's marty-ros2 checkout:

# Start the headless driver container, initially disconnected from Marty.
bash examples/onboard-pi/run-driver.sh
# Explicitly connect the configured Marty; this starts telemetry, not motion.
bash examples/onboard-pi/ros2.sh service call /marty/connect std_srvs/srv/Trigger '{}'
# Start the native camera as a transient service that survives terminal closure.
bash examples/onboard-pi/run-camera.sh
# Inspect driver output; Ctrl+C stops following logs, not the container.
sudo docker logs -f marty-driver

The container uses host networking and exposes the selected UART device. The camera defaults to 640×480 at 10 fps, H.264/MPEG-TS, TCP port 5000, with one video receiver. Optional Pi machine.env settings are CAMERA_WIDTH, CAMERA_HEIGHT, CAMERA_FPS, CAMERA_BITRATE, CAMERA_BIND and CAMERA_PORT.

Startup is manual, with no reboot autostart. The transient camera service restarts after a receiver disconnects; the receiver retries stream loss. The TCP example has no built-in authentication or encryption. ROS domain separation applies to ROS traffic, not access to that video socket.

Prepare the Linux workstation

Install ROS 2 Jazzy and its development tools using Install and build. For this camera workflow, the workstation only needs marty_interfaces and marty_camera; it does not need another Marty driver or MartyPy connection to the physical robot.

From a checkout on the workstation:

# Download the current source; use git pull --ff-only for an existing checkout.
git clone https://github.com/robotical/marty-ros2.git
cd marty-ros2
# Load the workstation's Jazzy installation.
source /opt/ros/jazzy/setup.bash
# Install dependencies for the ROS interfaces and network camera receiver.
rosdep install --from-paths src/marty_interfaces src/marty_camera --ignore-src -r -y
# Build the two required packages, without building/running a second Marty driver.
colcon build --base-paths src --packages-select marty_interfaces marty_camera --symlink-install

Set workstation config/machine.env as below. Replace PI_ADDRESS with the Pi's reachable address. Set the same domain as the Pi, and change the URL port if you changed CAMERA_PORT.

# Workstation ROS installation and shared DDS settings.
ROS_SETUP_FILE=/opt/ros/jazzy/setup.bash
ROS_DOMAIN_ID=0
RMW_IMPLEMENTATION=rmw_fastrtps_cpp
# TCP video source on the Pi, not the workstation itself.
MARTY_CAMERA_SOURCE=tcp://PI_ADDRESS:5000

If this checkout already has a working MARTY_PYTHON_VENV, it can remain. Do not set it to a nonexistent virtual environment for this two-package build. Each workstation helper sources scripts/setup_env.sh and the built overlay automatically.

Receive and view

In workstation Terminal 1:

# Decode video and publish the standard ROS image topic; leave running.
bash examples/onboard-pi/receive-camera.sh

In workstation Terminal 2:

# Open the saved RViz Image display; this script does not start another receiver.
bash examples/onboard-pi/view-camera.sh

The receiver node is /marty/camera/camera_receiver. It publishes /marty/camera/image_raw as sensor_msgs/msg/Image, BGR8, reliable, volatile, depth 1. RViz uses that QoS. Each decoded frame is published once; after stream loss the receiver waits for new frames rather than repeatedly publishing its cached image. One TCP receiver can serve multiple ROS image subscribers.

The driver uses the selected robot namespace (/marty here). The camera example's launch uses /marty/camera independently; changing the driver namespace does not automatically rename the camera topic.

Check both data paths

In another workstation terminal, from the same checkout:

# Load the same ROS domain and the workstation's compiled custom interfaces.
source scripts/setup_env.sh
# Find the Pi's driver and the workstation's camera receiver.
ros2 node list --no-daemon
# Inspect fresh driver connection/freshness/error information.
ros2 topic echo /marty/status marty_interfaces/msg/DriverStatus \
  --qos-reliability reliable --qos-durability transient_local --qos-depth 1 --once
# Read acceleration from the Pi driver; Ctrl+C stops only this subscriber.
ros2 topic echo /marty/imu/data_raw --field linear_acceleration --qos-reliability best_effort
# Inspect one image header and dimensions without dumping its byte array.
ros2 topic echo /marty/camera/image_raw sensor_msgs/msg/Image --once --no-arr \
  --qos-reliability reliable
# Measure received image publication rate; Ctrl+C stops the rate check.
ros2 topic hz /marty/camera/image_raw

The two echo/rate commands run sequentially only after stopping the preceding continuous command. Alternatively, use extra terminal/tmux panes. Marty driver measurements use best-effort QoS; the separate camera receiver is reliable. The image timestamp is workstation receipt time, not Pi exposure time, so it does not provide exposure-synchronised sensor fusion.

You can call /marty/connect remotely from a prepared workstation terminal using the same Trigger command; do not start a second physical driver there. No movement command is needed for the camera/telemetry check. Movement uses the existing Marty motion action when deliberately requested.

Frames, calibration and recovery

This example supplies no CameraInfo, camera calibration or camera-to-robot TF. RViz uses an Image display, not a calibrated Camera display. A global fixed-frame/TF warning does not by itself indicate an image failure. A remembered image in RViz is not proof of a live stream: check fresh timestamps or ros2 topic hz.

Symptom Check
Driver connected but no image Pi camera service, reachable stream URL/port, one TCP receiver, receiver log and image topic.
Image works but no Marty telemetry Pi container/UART, explicit connect, shared domain, DDS network reachability and best-effort telemetry subscriber.
Second video receiver cannot connect Expected single-client stream. Reuse the existing ROS image topic.
No driver found across machines Matching domain and middleware, network discovery allowed, and no guest/AP isolation blocking DDS.
Camera stream was interrupted Receiver should retry; inspect its log and sudo journalctl -u marty-camera -n 30 on the Pi.
Start command reports an existing container/unit It may already be running. Inspect it before starting a duplicate or using the stop commands below.

Stop and restart

On the workstation, Ctrl+C stops RViz in its terminal and the receiver in its own terminal. Closing RViz alone leaves the receiver, Pi driver and Pi camera service running.

On the Pi:

# Disconnect the robot while the driver container is still running.
bash examples/onboard-pi/ros2.sh service call /marty/disconnect std_srvs/srv/Trigger '{}'
# Stop the detached driver container (it was started with --rm).
sudo docker stop marty-driver
# Stop the manually started transient camera service.
sudo systemctl stop marty-camera

Restart using the normal Pi start commands, then restart the receiver/viewer if you stopped them. A Pi reboot requires starting both Pi processes again.

Using this alongside Axiom

Axiom continues to use its own host driver over its own USB/Wi-Fi connection. It can run on the workstation while Marty's driver runs on the Pi. To combine their ROS topics, use matching ROS settings and build the required interfaces on each consumer. An application can subscribe to Axiom sensors, Marty telemetry and images; neither firmware directly controls the other robot. A light panel, camera viewer and standalone Axiom servo demo are separate consumers, not prerequisites for each other.

Verified scope

The Pi 3B+/64-bit Bookworm expansion-UART setup and Ubuntu 24.04/Jazzy viewer were checked with live video near 10 fps, fresh Marty telemetry, remote connection and camera interruption/recovery. No movement commands were sent in that integration check. This does not validate every Pi/camera model, calibrated vision, joint-to-camera transforms or long-duration network performance. The implementation and restart commands are in the onboard-Pi repository guide.

Task Runner