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 zeroTwist()on/cmd_velis 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
/myappbefore forwarding, so write the app relative to its own root: HTML links, static resources, redirects andfetchmust use relative paths — absolute paths (/...) point outside/myapp/and break. - Auto-start:
/opt/physicar/userdata/myapp.shruns 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
- Simulator control API → PhysiCar Sim
- Cloud AI services (chat/realtime) → PhysiCar API
- What the parts are → Robot anatomy & sensors