Skip to content

PhysiCar ROS

The ROS 2 Jazzy stack that drives the robot (physicar-ros). The same stack runs in the simulator and on the real kit, so code written against these interfaces works unchanged in both.

Every interface on this page is runnable from the workspace notebook examples/physicar-ros.ipynb (kernel Python 3 (PhysiCar AI)).

ROS 2 topics

Sensors

Topic Type Notes
/camera/image_raw/compressed sensor_msgs/CompressedImage Camera image (JPEG)
/battery_state sensor_msgs/BatteryState 1 Hz, percentage is a 0–1 ratio
/imu sensor_msgs/Imu 50 Hz
/odom nav_msgs/Odometry fused (LiDAR + IMU)
/scan sensor_msgs/LaserScan raw
/scan_filtered sensor_msgs/LaserScan filtered

wait_for_message receives a single message without spinning — ideal for notebooks:

import rclpy
from rclpy.wait_for_message import wait_for_message
from sensor_msgs.msg import CompressedImage

rclpy.init()
node = rclpy.create_node("tutorial")
ok, image = wait_for_message(CompressedImage, node,
                             "/camera/image_raw/compressed", time_to_wait=2.0)

LiDAR QoS

The LiDAR publishes with best-effort sensor QoS — subscriptions to /scan and /scan_filtered must pass qos_profile_sensor_data to match.

Control

Topic Type Notes
/cmd_vel geometry_msgs/Twist linear.x = speed (m/s), angular.z = turn rate (rad/s), Ackermann conversion
/speed std_msgs/Float64 speed (m/s)
/steering std_msgs/Float64 steering angle (rad), + = left, max ±20° (±0.35 rad)
/camera/pan std_msgs/Float64 camera pan (rad), + = left, ±30° (±0.52 rad)
/camera/tilt std_msgs/Float64 camera tilt (rad), + = up, ±30° (±0.52 rad)
  • Safety watchdog — speed commands expire after the driver's cmd_timeout (~1 s) without renewal: publish periodically for sustained driving. The speed stops but the wheels keep their angle; a zero Twist() on /cmd_vel is an explicit stop that also recenters the steering.
  • Publisher discovery — a message published before the driver has discovered your publisher is lost. Wait for a subscriber first:
import time

from std_msgs.msg import Float64

speed_pub = node.create_publisher(Float64, "/speed", 10)
while speed_pub.get_subscription_count() == 0:
    time.sleep(0.1)

speed_pub.publish(Float64(data=0.5))

Web API

The same interfaces over HTTP, served by physicar_webserver. From code running in the workspace the base URL is http://localhost; interactive docs are at /docs (OpenAPI). Query endpoints support real-time streaming with ?stream=true — the camera streams MJPEG, others use SSE.

import requests

requests.post("http://localhost/speed", json={"value": 0.5, "duration": 2.0}, timeout=10)
jpg = requests.get("http://localhost/camera", params={"width": 480}).content

Sensor queries (GET)

Path Notes
/states Full state snapshot, select with ?include=odom,battery,imu
/speed · /steering m/s · rad
/odom · /battery · /imu sensor reads
/lidar scan with ranges, range_min/range_max, count; ?step= sets the angle step in degrees (default 1). 0° = front, +90° = left
/camera JPEG, resize with ?width/?height
/camera/pan · /camera/tilt angles (rad)

Control (POST)

Method Path Notes
POST /speed {"value": m/s, "duration": seconds?} — without duration it expires after cmd_timeout (~1 s) unless renewed. With duration the server keeps the command alive, publishes 0 at the end, and the response returns after the drive finishes
POST /steering {"value": rad}, persists until changed
POST /camera/pan · /camera/tilt {"value": rad}
WS /speed/stream · /steering/stream each frame is the same {"value": x} as the POST. Dead-man switch: on disconnect the value is zeroed, so a dead client can never leave the robot driving

Audio

Command-based playback on the robot speaker (in SIM, played in the browser viewer).

Method Path Notes
POST /audio/play one of url / path / data (base64 audio file); options volume (0–1), loop, replace
POST /audio/stop by id, or everything with {"all": true}
POST /audio/volume change volume of a playing item (id, volume 0–1)
POST /audio/duration duration of a playing item
GET /audio list of currently playing items
WS /audio/stream realtime PCM16 playback (?sample_rate=24000&channels=1&volume=1.0) — binary frames = raw PCM16, close = stop

Tool Server

A local FastAPI service that serves the AI chat's Python tools — the bundled robot.py (Web API mirror), sim.py (sim API), utils.py, plus the user-editable /opt/physicar/userdata/custom_tools.py. It runs on loopback only and is reachable through nginx at /physicar-ext/ (requests from the network are denied). A crash or an intentional reload comes back within a second, and a broken custom script never takes the server down — the last working module keeps serving while the import error is reported.

Method Path Notes
GET /physicar-ext/tools tool list + parameter schemas (+ per-script import errors)
POST /physicar-ext/tools/<name> run a tool — {"args": {...}, "session": "<chat session>"}
POST /physicar-ext/wake redeem a one-shot wake ticket — {"wake_id": "...", "note": "..."}
POST /physicar-ext/wake/status ticket state ({"wake_id"}) or a session's outstanding tickets ({"session"})
GET /physicar-ext/health loaded modules, import errors, process RSS
POST /physicar-ext/reload restart the interpreter — picks up newly installed libraries or replaced model weights

Wake tickets let tool code wake the AI later: reserve a ticket during a tool call (from pcwake import reserve, redeem, or the utils_wake_reserve tool), hand the id to a background thread, and redeem it when the event happens — the chat session that reserved it starts an automatic turn. Tickets are one-shot (a second redeem is a no-op) and in-memory.

MyApp

Launch your own web app on port 5000 and it becomes accessible at /myapp/ (the App page's MYAPP tab).

  • nginx strips /myapp before forwarding, so write the app relative to its own root: HTML links, static resources, redirects and fetch must use relative paths — absolute paths (/...) point outside /myapp/ and break.
  • Auto-start: /opt/physicar/userdata/myapp.sh runs at boot (the command that launches the app); its log is /opt/physicar/userdata/myapp.log.
  • Calling PhysiCar AI services from a MyApp page — see PhysiCar API.

Learn more

AI