Architecture
AR4 Physical-AI is a VLA (Vision-Language-Action) platform layered on top of the AR4 ROS driver and LeRobot. The key architectural decisions:
- LeRobot-native data and policies — records LeRobot-format datasets and fine-tunes LeRobot policies such as SmolVLA
- Submodule for driver — upstream
ar4_ros_driverstays independently updateable - Docker-first — multi-stage GPU containers (base → overlay → dev) with docker-compose orchestration
- Simulation-first — physics-enabled Gazebo world with gravity, contact properties, and graspable objects for policy development
- Zenoh middleware — decouples non-ROS components from DDS for future inference pipelines
- Explicit control paths — recording and inference use different controller stacks, documented below instead of hidden behind one generic bridge
System Overview
C4 Model
The same system drawn at two C4 levels: the context (who uses it and what it depends on) and the containers (the deployable pieces and the data pipeline between them).
System Context (L1)
Editable diagram source: images/c4-context.c4.yaml
Containers (L2)
Editable diagram source: images/c4-container.c4.yaml
Control and Data Paths
The project has four separate paths. They share the same robot, cameras, joint state topic, and LeRobot dataset schema, but they do not all use MoveIt in the same way.
Editable diagram source: images/c4-control-paths.c4.yaml
| Path | Purpose | Policy/source | Controller path | MoveIt role | Output recorded or executed |
|---|---|---|---|---|---|
| Human teleoperation | Human demonstrations | Gamepad operator | Gamepad -> servo_teleop_node -> MoveIt Servo -> /joint_trajectory_controller/joint_trajectory | Online velocity control, joint-limit/singularity handling, optional collision checks | Recorded LeRobot frames with realised joint positions as action |
| MoveIt expert | Synthetic demonstrations | Scripted planner/IK expert | moveit_record_node -> MoveIt IK/planning services -> direct JointTrajectory execution | Planning, IK, planning-scene collision model; Servo is paused to avoid command contention | Recorded LeRobot frames with realised joint positions as action |
| SmolVLA inference, direct | Current learned-policy rollout | Fine-tuned SmolVLA | cameras + /joint_states -> SmolVLABackend.select_action() -> inference_node -> /joint_trajectory_controller/joint_trajectory | Stack may be launched for robot state/RViz, but Servo is paused and not in the command path | Executed absolute joint targets [j1..j6, gripper] |
| SmolVLA inference, Servo-filtered | Proposed safer learned-policy rollout | Fine-tuned SmolVLA plus adapter | cameras + /joint_states -> SmolVLA action chunk -> adapter -> MoveIt Servo input topic -> JTC | Online safety/filtering layer for joint limits, singularities, and collision proximity | Executed Servo-filtered trajectory or halt |
The distinction matters because "MoveIt is running" is not the same as "MoveIt is in the
policy control loop." In the current SmolVLA inference implementation, the policy publishes
single-point JointTrajectory commands directly to the joint trajectory controller, and the
node pauses MoveIt Servo at startup to prevent command conflicts.
A. Human Teleoperation Data
Human teleoperation uses MoveIt Servo as the live controller. The gamepad produces Cartesian or joint jog commands, Servo converts them into smooth joint trajectory commands, and the recorder saves the resulting robot state at each tick. The dataset label is the realised joint configuration, not the joystick velocity command. This keeps human demonstrations schema-compatible with scripted demonstrations and SmolVLA inference:
B. MoveIt Expert Data
The scripted expert generates synthetic demonstrations. It uses MoveIt for IK, planning, and
the planning scene, but it does not use Servo as the streaming controller for the episode.
moveit_record_node pauses Servo, computes target joint poses or Cartesian segments, and sends
planned or direct JointTrajectory commands to the joint trajectory controller. The recorder
again writes the realised joint positions as the action labels:
This is also where induced waypoint randomness belongs for synthetic data. The randomness is
part of the demonstrated trajectory before recording, so the learner sees multiple valid
ways to solve the same (object, shelf) placement instead of one deterministic joint replay.
C. SmolVLA Inference Without MoveIt
This is the path implemented by ar4_policy_server/ros_bridge/inference_node.py today.
SmolVLA receives camera frames, the current joint state, and the language instruction. The
LeRobot pre/post-processors normalize the observation and denormalize the action. The node
then streams the resulting absolute joint targets directly to the joint trajectory controller:
SmolVLA still uses its internal action chunking, but this repo consumes the chunk as a queue
of absolute joint targets. It does not currently convert chunks into JointJog,
TwistStamped, or Servo input messages.
D. SmolVLA Inference With MoveIt
This is the architecture to build if learned-policy actions should be filtered by MoveIt Servo before reaching the robot. It needs an explicit adapter between SmolVLA and Servo:
That adapter must decide whether SmolVLA's absolute joint targets are converted into joint deltas, joint velocities, Cartesian twists, or dense short-horizon trajectory points. It also has to manage action chunk timing so inference latency does not stall Servo's command stream. This path is not the current implementation; it is a separate safety-filtered inference mode.
Docker Infrastructure
All services run in Docker containers orchestrated by docker-compose.yaml. The Dockerfile uses a multi-stage build:
| Service | Image | Purpose |
|---|---|---|
sim-tabletop | overlay | Gazebo GUI simulation |
sim-tabletop-headless | overlay | Server-only Gazebo (CI, headless) |
moveit | overlay | MoveIt2 motion planning + RViz2 |
hardware | base | Real AR4 hardware driver (calibrates on startup) |
moveit-hardware | base | MoveIt2 + RViz2 for real hardware (auto-starts hardware) |
foxglove-bridge | overlay | WebSocket bridge for Foxglove Studio (:8765) |
zenoh-router | eclipse/zenoh | Central Zenoh broker with in-memory storage (:7447) |
zenoh-bridge | overlay | DDS-to-Zenoh bridge for cross-container pub/sub |
dev | dev | VS Code devcontainer with source mounts |
Zenoh Middleware
Zenoh provides a lightweight pub/sub transport layer that decouples non-ROS components (like the Optuna PID tuner) from the DDS discovery mesh. This avoids requiring ROS 2 in every container.