---json
{"name": "Onboard Pi and camera"}
---
====== Onboard Pi, camera and RViz ======
{{indexmenu_n>40}}
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 =====
{{ :martyv2:ros2:onboard-pi-camera-49fb96e.png?900&nolink |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 [[:martyv2:userguides:raspberry_pi:start|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 [[:martyv2:ros2:getting_started#install_and_build|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 [[:martyv2:ros2:interfaces#movement_action|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 [[:axiom:ros2:lighting|light panel]], camera viewer and [[:axiom:ros2:servo_slider|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 [[https://github.com/robotical/marty-ros2/blob/main/examples/onboard-pi/README.md|the onboard-Pi repository guide]].