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¶
Option A — Docker Compose (recommended)¶
# 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:
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:
- Instantiates a
RobotNodethat publishes synthetic/odom,/battery_state,/scan,/imu/data,/cmd_vel,/gps/fix, and/delivery/statusmessages at 10 Hz - Starts the
PhyTraceBridgeNodesubscribed to those topics - Runs for
num_tickscycles, collecting published events - 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:
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:
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:
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