Skip to content

PhyTrace ROS2 Bridge — Onboarding Guide

The PhyTrace ROS2 Bridge is a drop-in container node that connects any ROS2 robot stack to PhyCloud. It subscribes to standard ROS2 topics, normalizes every message to the PhyTrace Unified Data Model (UDM), and streams batched telemetry events to PhyCloud over HTTP — no changes to your existing robot code required.


1. Overview

Modern robot deployments produce telemetry across dozens of topics and message types. Without a common schema, that data is siloed and impossible to analyze uniformly across a fleet. The PhyTrace ROS2 Bridge solves this by:

  • Translating ROS2 messages into structured UDM events in real time
  • Batching events for efficient HTTP transmission
  • Decoupling data collection from robot application logic — it runs as its own container
  • Requiring no ROS2 installation to develop or test against, thanks to a built-in mock ROS2 layer

2. Architecture

┌──────────────────────────────────────────────────────────────────────┐
│  ROS2 Robot Stack                                                    │
│                                                                      │
│  /odom ──────────────┐                                               │
│  /battery_state ─────┤                                               │
│  /scan ──────────────┤   ┌──────────────────────────┐               │
│  /imu/data ──────────┼──▶│   PhyTraceBridgeNode     │               │
│  /cmd_vel ───────────┤   │   (phytrace_ros)         │               │
│  /gps/fix ───────────┤   │                          │               │
│  /diagnostics ───────┤   │  topic_handlers.py       │               │
│  /goal_pose ─────────┤   │    ↓ (ROS msg → dict)    │               │
│  /joint_states ──────┤   │  udm_builder.py          │               │
│  /delivery/status ───┘   │    ↓ (dict → UDMEvent)   │               │
│                          │  cloud_sink.py ──────────┼──▶  PhyCloud  │
│                          └──────────────────────────┘    HTTP API   │
└──────────────────────────────────────────────────────────────────────┘

Data flow summary:

ROS2 Robot Topics
 topic_handlers.py    ← pure functions, no ROS2 state
      │  (typed dicts)
 UDMBuilder           ← aggregates latest data per topic
      │  (UDMEvent)
 CloudSink            ← batches + HTTP POSTs to PhyCloud
 PhyCloud  ──▶  PhySafe  ──▶  PhyComp  ──▶  Phylyze

3. Prerequisites

Requirement Version Notes
Docker 20.10+ Required for containerized deployment
Docker Compose v2.0+ docker compose (plugin) or docker-compose (standalone)
Python 3.10+ For local development without Docker
ROS2 Humble (optional) Only needed when connecting to a real robot; mock layer covers testing
PhyCloud account Obtain PHYCLOUD_ENDPOINT and PHYCLOUD_API_KEY from your PhyCloud dashboard

4. Quick Start

# 1. Clone the repository
git clone https://github.com/chrisherczeg/PhyWare.git
cd PhyWare/ros2_bridge

# 2. Set credentials (or export them in your shell)
export PHYCLOUD_ENDPOINT=https://api.phycloud.phyware.io/v1
export PHYCLOUD_API_KEY=your-api-key
export SOURCE_ID=my-robot-001          # unique robot identifier
export SOURCE_TYPE=delivery_robot      # robot class

# 3. Build and run
docker compose up --build

The bridge connects to the host network (network_mode: host) so it sees the ROS2 DDS traffic from your robot stack automatically.

Development mode — spin up a local PhyCloud stub alongside the bridge:

docker compose --profile dev up --build
# Stub echoes received events at http://localhost:8000

Option B — Local Python (no ROS2 required)

Use this path during development or CI — the built-in simulation replaces a real robot.

cd PhyWare/ros2_bridge

# Install the PhyTrace SDK from source
pip install -e ../PhyTrace/python-sdk/

# Install bridge dependencies
pip install -r requirements.txt
pip install -e .

# Run the delivery robot simulation (100 ticks)
python -c "
from delivery_robot_sim.sim_runner import run_simulation
stats = run_simulation(num_ticks=100)
print(stats)
"

5. Topic Mapping

The bridge subscribes to the following standard ROS2 topics. Topics that are absent on the robot are silently skipped — no configuration change needed.

Topic ROS2 Type UDM Domain Description
/odom nav_msgs/Odometry Location, Motion Robot odometry — position, orientation, and velocity in the local frame
/battery_state sensor_msgs/BatteryState Power Battery voltage, current, state-of-charge, and charging status
/scan sensor_msgs/LaserScan Perception 2D/3D laser scanner — range array, min/max range, angle bounds
/imu/data sensor_msgs/Imu Motion IMU orientation (quaternion), angular velocity, linear acceleration
/cmd_vel geometry_msgs/Twist Motion Commanded linear and angular velocity from the nav stack
/gps/fix sensor_msgs/NavSatFix Location WGS84 GPS fix — latitude, longitude, altitude, covariance
/diagnostics diagnostic_msgs/DiagnosticArray Operational Hardware/software diagnostic status and error messages
/goal_pose geometry_msgs/PoseStamped Navigation Current navigation goal in the map frame
/joint_states sensor_msgs/JointState Actuators Joint names, positions, velocities, and efforts
/delivery/status std_msgs/String (JSON) Operational, Payload Delivery task status — order ID, state, payload metadata

6. Configuration

All settings can be provided as environment variables (Docker/systemd) or as ROS2 launch parameters (see launch/bridge_only.launch.py).

Variable ROS2 Param Default Description
PHYCLOUD_ENDPOINT phycloud_endpoint http://localhost:8000/api/v1 Base URL of the PhyCloud ingestion API
PHYCLOUD_API_KEY phycloud_api_key (empty) API key for authentication
SOURCE_ID / ROBOT_ID source_id delivery-robot-001 Globally unique identifier for this robot
SOURCE_TYPE / ROBOT_TYPE source_type delivery_robot UDM source type — one of delivery_robot, amr, agv, drone, humanoid, arm
FLEET_ID fleet_id (none) Fleet grouping identifier
SITE_ID site_id (none) Site or facility identifier
SAMPLE_RATE_HZ sample_rate_hz 5.0 How often (Hz) to publish a UDM event to PhyCloud
BATCH_SIZE batch_size 10 Number of events per HTTP batch request

Example .env file

PHYCLOUD_ENDPOINT=https://api.phycloud.phyware.io/v1
PHYCLOUD_API_KEY=phk_live_xxxxxxxxxxxxxxxx
SOURCE_ID=warehouse-amr-07
SOURCE_TYPE=amr
FLEET_ID=chicago-dc-fleet
SITE_ID=chicago-dc-01
SAMPLE_RATE_HZ=10.0
BATCH_SIZE=20

Pass it to Docker Compose with docker compose --env-file .env up.


7. Running the Delivery Robot Simulation

The delivery_robot_sim package ships a fully self-contained simulation — no physical robot or ROS2 installation required.

cd ros2_bridge

# Quick run — prints stats after 100 ticks
python -c "
from delivery_robot_sim.sim_runner import run_simulation
stats = run_simulation(num_ticks=100)
import json; print(json.dumps(stats, indent=2))
"

# Or run with the full launch file (requires ROS2)
ros2 launch phytrace_ros delivery_robot.launch.py \
  phycloud_endpoint:=http://localhost:8000/api/v1 \
  source_id:=sim-robot-1

What the simulation does:

  1. Instantiates a RobotNode that publishes synthetic /odom, /battery_state, /scan, /imu/data, /cmd_vel, /gps/fix, and /delivery/status messages at 10 Hz
  2. Starts the PhyTraceBridgeNode subscribed to those topics
  3. Runs for num_ticks cycles, collecting published events
  4. Returns a stats dict: {events_sent, events_failed, topics_seen, duration_s}

8. Connecting to a Real Robot

Docker Compose (add to existing stack)

Add the following service to your robot's docker-compose.yml:

services:
  phytrace-bridge:
    image: ghcr.io/chrisherczeg/phyware/phytrace-ros2-bridge:latest
    network_mode: host          # share ROS2 DDS network with robot containers
    environment:
      PHYCLOUD_ENDPOINT: https://api.phycloud.phyware.io/v1
      PHYCLOUD_API_KEY: ${PHYCLOUD_API_KEY}
      SOURCE_ID: ${ROBOT_HOSTNAME:-robot-001}
      SOURCE_TYPE: amr
      FLEET_ID: ${FLEET_ID}
      SITE_ID: ${SITE_ID}
    restart: unless-stopped

ROS2 Launch (bridge only, robot already running)

ros2 launch phytrace_ros bridge_only.launch.py \
  phycloud_endpoint:=https://api.phycloud.phyware.io/v1 \
  phycloud_api_key:=phk_live_xxx \
  source_id:=my-robot-1 \
  source_type:=amr \
  fleet_id:=my-fleet \
  sample_rate_hz:=10.0

systemd (bare-metal ROS2)

[Unit]
Description=PhyTrace ROS2 Bridge
After=network.target

[Service]
EnvironmentFile=/etc/phytrace/bridge.env
ExecStart=/opt/ros/humble/bin/ros2 launch phytrace_ros bridge_only.launch.py
Restart=on-failure

[Install]
WantedBy=multi-user.target

9. What PhyCloud Receives

Every SAMPLE_RATE_HZ interval the bridge assembles a single UDMEvent from the latest buffered values for all active topics and POSTs it (in batches of BATCH_SIZE) to PHYCLOUD_ENDPOINT/events.

Example UDM event JSON (delivery robot, all domains populated):

{
  "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "event_type": "telemetry.periodic",
  "schema_version": "1.0.0",
  "captured_at": "2025-01-15T14:23:01.042Z",
  "source_id": "warehouse-bot-07",
  "source_type": "delivery_robot",
  "identity": {
    "source_id": "warehouse-bot-07",
    "source_type": "delivery_robot",
    "fleet_id": "chicago-dc-fleet",
    "site_id": "chicago-dc-01"
  },
  "location": {
    "latitude": 41.8781,
    "longitude": -87.6298,
    "altitude_m": 182.5,
    "heading_deg": 45.2,
    "coordinate_system": "WGS84",
    "local": {
      "coordinate_frame": "map",
      "x_m": 12.34,
      "y_m": 5.67,
      "z_m": 0.0,
      "yaw_deg": 45.2
    }
  },
  "motion": {
    "linear_velocity": { "x_mps": 0.85, "y_mps": 0.0, "z_mps": 0.0 },
    "angular_velocity": { "yaw_dps": 2.1 },
    "commanded_velocity": { "linear_x_mps": 1.0, "angular_z_rps": 0.0 },
    "motion_state": "moving"
  },
  "power": {
    "battery": {
      "voltage_v": 24.1,
      "current_a": -2.3,
      "state_of_charge_pct": 82.5,
      "temperature_c": 28.4
    },
    "charging": { "is_charging": false },
    "power_state": "on_battery"
  },
  "operational": {
    "state": "autonomous",
    "task": {
      "task_id": "order-8821",
      "task_type": "delivery",
      "status": "in_progress",
      "payload_id": "pkg-5501"
    },
    "errors": []
  },
  "navigation": {
    "goal": {
      "frame_id": "map",
      "x_m": 45.0,
      "y_m": 22.0,
      "yaw_deg": 90.0
    }
  },
  "perception": {
    "lidar": [
      {
        "sensor_id": "front_lidar",
        "min_range_m": 0.12,
        "max_range_m": 12.0,
        "num_points": 360
      }
    ],
    "imu": {
      "orientation_deg": { "roll": 0.1, "pitch": -0.2, "yaw": 45.2 },
      "angular_velocity_dps": { "x": 0.0, "y": 0.0, "z": 2.1 },
      "linear_acceleration_mps2": { "x": 0.05, "y": 0.01, "z": 9.81 }
    },
    "gps": {
      "latitude": 41.8781,
      "longitude": -87.6298,
      "altitude_m": 182.5,
      "fix_type": "3d_fix",
      "num_satellites": 9
    }
  },
  "actuators": {
    "joints": [
      { "name": "left_wheel", "position_rad": 3.14, "velocity_rps": 2.5 },
      { "name": "right_wheel", "position_rad": 3.14, "velocity_rps": 2.5 }
    ]
  },
  "payload": {
    "order_id": "order-8821",
    "package_id": "pkg-5501",
    "delivery_state": "in_transit",
    "destination": "dock-B3"
  }
}

10. Testing

The test suite requires no ROS2 installation — the delivery_robot_sim.mock_ros module provides a full rclpy shim.

cd ros2_bridge

# Install test dependencies
pip install pytest pytest-mock httpx
pip install -e ../PhyTrace/python-sdk/
pip install -r requirements.txt
pip install -e .

# Run all tests
python -m pytest tests/ -v

# Run only unit tests
python -m pytest tests/unit/ -v

# Run only integration tests
python -m pytest tests/integration/ -v

# Run with coverage
pip install pytest-cov
python -m pytest tests/ --cov=phytrace_ros --cov-report=term-missing

Test layout:

tests/
├── unit/
│   ├── test_topic_handlers.py    # ROS msg dict → domain dict conversions
│   ├── test_udm_builder.py       # UDMBuilder assembly logic
│   └── test_cloud_sink.py        # HTTP batching and mock sink
└── integration/
    └── test_ros_bridge_integration.py  # End-to-end: sim robot → bridge → captured events

11. Troubleshooting

Bridge starts but no events appear in PhyCloud

Symptom Likely cause Fix
PHYCLOUD_API_KEY not set Missing auth Export PHYCLOUD_API_KEY or add to .env
Connection refused to PhyCloud Wrong endpoint Verify PHYCLOUD_ENDPOINT is reachable from the container
Events accumulating but not flushing BATCH_SIZE too large Lower BATCH_SIZE or check SAMPLE_RATE_HZ

No topics received from the robot

# Check that the bridge sees the ROS2 graph
docker exec phytrace_ros2_bridge ros2 topic list

# Verify the bridge container shares the robot's DDS network
# It must use network_mode: host or be on the same bridge network

If no topics appear, ensure the bridge and robot containers share the same RMW_IMPLEMENTATION and ROS_DOMAIN_ID.

PhyTrace SDK not available – using plain-dict fallback

This warning is printed when the PhyTrace Python SDK is not installed. Events will still be sent as plain JSON dicts but without strong schema validation. Install the SDK:

pip install phytrace-sdk
# or from source:
pip install -e ../PhyTrace/python-sdk/

UDM validation errors

Events failing UDM validation are logged at WARNING level with the field path and value. Run with --log-level DEBUG for full details:

ros2 launch phytrace_ros bridge_only.launch.py --ros-args --log-level debug

High memory usage / event backlog

If PhyCloud is unreachable, events queue in memory. To cap queue size, reduce BATCH_SIZE and increase SAMPLE_RATE_HZ, or enable the local PhyCloud stub for development:

docker compose --profile dev up

Project Structure

ros2_bridge/
├── phytrace_ros/              # Bridge node and UDM pipeline
│   ├── bridge_node.py         # Main ROS2 node — wires topics → builder → sink
│   ├── topic_handlers.py      # Pure functions: ROS msg → typed dict
│   ├── udm_builder.py         # Aggregates topic dicts → UDMEvent
│   └── cloud_sink.py          # HTTP and mock sinks with batching
├── delivery_robot_sim/        # Self-contained simulation (no real ROS2 needed)
│   ├── mock_ros.py            # rclpy drop-in mock
│   ├── robot_node.py          # Synthetic delivery robot publisher
│   └── sim_runner.py          # Orchestrates robot + bridge + stats
├── docs/
│   └── ros2_onboarding.md     # This file
├── tests/
│   ├── unit/
│   └── integration/
├── launch/
│   ├── bridge_only.launch.py      # Bridge only (attach to running robot)
│   └── delivery_robot.launch.py   # Bridge + simulation
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── setup.py