image_publisher_client — the ROS 2 host side

This directory holds the client half of the camera ingestion stack. The server halves (one per hardware pipeline) live elsewhere — see the parent overview.

The client side is uniform: for every MJPG URL coming off an edge node, run exactly one image_publisher_node in its own tmux pane (or its own Node in a launch file). Each pane:

  • decodes the MJPG / JPG stream,
  • republishes it as sensor_msgs/Image on <__ns>/image_raw,
  • runs at the rate set by publish_rate.

The single most important rule is that publish_rate must equal the edge capture rate exactly. See Why publish_rate matters below.


Files in this directory

FileRuntimePurpose
image_publishers.tmuxp.ymlros2 on PATHDefault tmuxp session — IMX708 + ESP32-S3 mix on the kernel/ROS host
pixi_image_publishers.ymlMac/laptop with pixiSame idea, but every command is prefixed with pixi run -e kilted ros2
esp32s3_eth.tmuxp.ymlros2 on PATHWired-ETH ESP32-S3 fleet + Lepton thermal MJPG, plus a browser pane per cam
ROS2_LAUNCH/image_publishers.launch.pyros2 launchEquivalent to image_publishers.tmuxp.yml but as a single ROS 2 launch file

A second, ESP32-S3-only Python launch file lives one level up: ../xiao_sense_esp32s3_eyes.py — for the WiFi XIAO eyes (172.31.1.139/142/143/144 @ :81/stream, 16 Hz).


Choosing between image_publishers.tmuxp.yml and pixi_image_publishers.yml

Functionally identical. The only difference is how ros2 is invoked:

# image_publishers.tmuxp.yml — assumes ROS is installed system-wide
- shell_command:
    - ros2 run image_publisher image_publisher_node ...
# pixi_image_publishers.yml — assumes pixi env "kilted" is set up locally
- shell_command:
    - pixi run -e kilted ros2 run image_publisher image_publisher_node ...

Use cases:

  • image_publishers.tmuxp.yml — running on the ROS host (the ros2 Docker container, a real Ubuntu/ROS box, or any environment where which ros2 works at the prompt). This is the production launcher when the kernel host comes up.
  • pixi_image_publishers.yml — running on macOS / a laptop during development. There is no native ROS 2 install; pixi brings in a per-project ROS distribution (here, the kilted env defined in the repo's pixi config). Use this when you want to view the streams from your laptop using rqt_image_view or Foxglove without spinning up a Docker container.

The pixi variant is intentionally a subset of the full client — it only launches the two cameras you typically need to monitor from a laptop (172.31.1.97:8000 and :8001).


Camera namespace map

The default tmuxp session (image_publishers.tmuxp.yml) publishes the following ROS 2 topics. 172.31.1.x here are the static IPs of the Raspberry Pi hosts running simple_picamera2_streamer/app.py (IMX708 cams over CSI). Each Pi may host more than one streamer on different ports.

ROS namespaceEdge URLHardwareEdge process
/cam0http://172.31.1.96:8000/streamIMX708 + Pisimple_picamera2_streamer
/cam1http://172.31.1.96:8001/streamIMX708 + Pisimple_picamera2_streamer
/cam2http://172.31.1.97:8000/streamIMX708 + Pisimple_picamera2_streamer
/cam3http://172.31.1.97:8001/streamIMX708 + Pisimple_picamera2_streamer
/cam4http://172.31.1.98:8000/streamIMX708 + Pisimple_picamera2_streamer
/cam5http://172.31.1.99:8000/streamIMX708 + Pisimple_picamera2_streamer

All published at publish_rate:=8. — matching the IMX708 streamer's hard 8 Hz cap.

The ESP32-S3 XIAO fleet (xiao_sense_esp32s3_eyes.py):

ROS namespaceEdge URLHardwareFirmware
/xiao_139http://172.31.1.139:81/streamXIAO ESP32-S3 + OV…CameraWebServer_for_esp-arduino_3.0.x.ino
/xiao_142http://172.31.1.142:81/streamXIAO ESP32-S3 + OV…"
/xiao_143http://172.31.1.143:81/streamXIAO ESP32-S3 + OV…"
/xiao_144http://172.31.1.144:81/streamXIAO ESP32-S3 + OV…"

All published at publish_rate:=16..

The wired-ETH ESP32-S3 + Lepton mix (esp32s3_eth.tmuxp.yml):

ROS namespaceEdge URLNotes
/TSO_0http://172.31.1.130:81/streamESP32-S3 ETH @ 24 Hz
/TSO_1http://172.31.1.79:81/streamESP32-S3 ETH @ 24 Hz
/TSO_2http://172.31.1.80:81/streamESP32-S3 ETH @ 24 Hz
/cams/table_0http://172.31.1.81:81/streamESP32-S3 ETH @ 24 Hz
/cams/table_1http://172.31.1.59:81/streamESP32-S3 ETH @ 24 Hz
/thermhttp://172.31.1.97:9009/streamLepton 3.5 thermal MJPG @ 12 Hz

IP convention reminder. ESP32-S3 boards on this network are bound to 172.31.1.<DEVICE_ID> by their firmware (#define DEVICE_ID …). Pi cam hosts use whatever DHCP/static you've assigned them; the "port +1 per cam" trick (:8000, :8001) is what lets one Pi host two IMX708 streamers.


Running them

# default — ROS host / docker container
tmuxp load ros2_ws/launch/image_publisher_client/image_publishers.tmuxp.yml

# from a Mac/laptop (no system ROS)
tmuxp load ros2_ws/launch/image_publisher_client/pixi_image_publishers.yml

# wired ESP32-S3 fleet + Lepton thermal
tmuxp load ros2_ws/launch/image_publisher_client/esp32s3_eth.tmuxp.yml

# WiFi XIAO ESP32-S3 fleet
ros2 launch ros2_ws/launch/xiao_sense_esp32s3_eyes.py

Verify in another shell:

ros2 topic list | grep image_raw
ros2 topic hz /cam0/image_raw          # should report ~8 Hz
ros2 run rqt_image_view rqt_image_view /cam0/image_raw

Why publish_rate matters

image_publisher_node opens MJPG URLs through OpenCV (cv::VideoCapture), which buffers decoded frames internally. If publish_rate is slower than the rate the edge produces frames, that buffer fills up and the node republishes frames from seconds — sometimes tens of seconds — in the past.

The downstream symptom is "smooth but wrong" video: rqt_image_view / Foxglove / RViz show motion that looks fluid but lags the real world by a slowly growing offset.

The fix is to match the rate exactly. Per pipeline:

Edge pipelineEdge rateRequired publish_rate
simple_picamera2_streamer (IMX708, current code)8 Hz8.
CameraWebServer_for_esp-arduino_3.0.x (XIAO ESP32-S3, WiFi, current code)~16 Hz16.
ESP32-S3 over wired ETH (esp32s3_eth.tmuxp.yml)24 Hz24.
Lepton 3.5 thermal MJPG9–12 Hz12.

If you bump the edge rate in the firmware / streamer, bump it here in lockstep — every tmuxp file and every launch file in this directory.


Related