Demo
Fastest path to watch the robot restock:
just demo # sensor-driven: the shipped default, no evidence pins
just demo 3 # at most three campaign cycles
just demo gt:=true # labelled opt-in: the dense ground-truth demo
This brings up Gazebo, MoveIt, RViz, and the coordinator, waits until the coordinator reports
ready, then starts the autonomous mode loop. By default it runs the shipped sensor-driven
configuration — the same defaults as a plain baseline.launch.py: the wrist lane survey and
the wrist tray-confirm duty own the world state's ingest topics, simulator ground truth is
demoted to /perception/ground_truth/* where only the error evaluators read it, the evidence
ages are the shipped 180 s profile, and the overhead camera is on for obstacle detection
only (its object pipeline stays off). Nothing about the default path is pinned by the demo
script: what you watch is what the product ships.
At start of run the robot sweeps every front-lane station with its wrist camera, measures each
lane's deficit against the declared target, and submits unaddressed /restock_product goals.
A transfer goes out only after that survey's close-range confirmation (or against an
already-admitted candidate) while every lane's evidence is inside its validity horizon. The
loop parks in IDLE when every lane reaches its target.
The ground-truth opt-in
just demo gt:=true switches to the labelled ground-truth mode: it prints
GROUND-TRUTH MODE (opt-in) and pins ground truth back onto the ingest topics with both wrist
tray duties off (one producer per topic), on the dense fixture — six partially stocked front
lanes and three dense-but-partial back grids, so a full run makes thirteen transfers before the
front is full. Ground truth refreshes continuously there, which is why that mode can pin the
whole pre-flip evidence profile the deterministic demo was written against —
product_observation_max_age 2.0 s, selection_object_max_age_ms 2000,
maximum_observation_age_ms 2000 and lane_evidence_validity_ms 60000 — instead of the shipped
180 s sensor profile. The default path pins none of them.
Use it when you want the long, busy run; the default is what the system actually runs.
What you see
RViz opens with a predefined three-pane layout instead of the plain MoveIt window:
- Centre — planning view. The same MoveIt
MotionPlanningdisplay asjust launch-baseline: robot model, planning scene, fixed frameworld. - Right, top — Overhead Camera. The fixed shelf-facing RGB-D camera's colour stream on
/overhead_camera/image(960x720, about 6 Hz): the whole shelf face, so you can watch deficits empty and transfers land. On the default path this camera is feeding obstacle detection, not product perception. - Right, bottom — Wrist Camera. The gripper-mounted colour stream on
/wrist_camera/image(1280x720, about 5 Hz): whatever the arm is looking at — lane survey stations and tray confirmation viewpoints as they happen. On the default path this is the camera the evidence comes from. - Left — Displays and Views panels, docked rather than floating.
Both image panes subscribe reliably, matching the Gazebo→ROS bridge's publisher. The layout lives
in ros_ws/src/restocker_bringup/rviz/demo_camera_view.rviz and is selected only by this demo
through the rviz_config launch argument; just launch-baseline and every headless test or
benchmark keep the default moveit.rviz, so nothing else changes behaviour. Depth images,
camera_info, and Gazebo's own GUI are not part of the layout.
The optional number limits mode-loop cycles, and every progress cycle submits at most one
transfer goal — so just demo 3 moves about three products at most, not three survey-and-drain
phases. With no number the campaign continues until Ctrl-C.
What to expect
The default fixture is the sensor acceptance one: three lanes each wanting one product, one
product per class in the tray behind, so the loop needs three transfers to reach
PHASE_FRONT_FULL with zero deficit and then idles until evidence changes. That is the table the
acceptance test proves. Camera identity also names the dense tray's repeated products, by their
stocking position. However, the dense scenario has not yet completed a sensor-driven run, so it
stays the ground-truth opt-in (see the repository README's Known limitations). The dense
sensor-driven end-to-end check exists as its own serialized gate, just dense-gate, which is
excluded by label from just test and nix flake check — a green default suite never implies
that coverage (see Tests and benchmarks).
In either mode the acceptance gate is the same shape: fill every measured deficit against the
declared targets and end in PHASE_FRONT_FULL, not PHASE_STOCK_EXHAUSTED or
PHASE_BLOCKED. A full dense run takes about 45 minutes; the default's three transfers take a
few minutes after the cold-start sweep. Individual transfers fail closed if motion, evidence, or
outcome verification is invalid; a blocked phase returns to surveying on the next cycle instead
of claiming success or retrying a failed pair in a tight loop.
Measured reliability of that loop on the default path: a predeclared 24-run campaign on
2026-09-29 passed 14/24 (58.3 %, Wilson 95 % 38.8–75.5 %); every failure ended at a
named refusal (the tray survey finding no candidate, the recovery ladder ending in
motion inhibited, or the survey-recovery skip budget being spent).
A later 24-attempt weld population, measured at the time of the release, passed 19/24 (79.2%, Wilson 95% 59.5–90.8%). The comparison to 14/24 is not statistically separated (Fisher p=0.2124), and passing console summaries do not retain enough detail to compare intermediate refusals. These are historical fixture-specific outcomes, not a current reliability guarantee; see the evidence limitations.
Requirements
- Graphical session:
DISPLAYorWAYLAND_DISPLAY(Xwayland counts) - Built workspace (
just build) - Flake shell active (
direnvornix develop .)
Without a display, scripts/demo.bash exits immediately and points you at headless alternatives.
Isolation
The demo leases a ROS_DOMAIN_ID and GZ_PARTITION via scripts/domain_isolation.py. A second
terminal that inspects topics must use the same domain the demo printed.
Under the hood
- Launch
baseline.launch.pywithmotion_enabled:=true,autonomous_campaign:=true,campaign_restart_acknowledged:=true(the demo world is fresh, so the campaign's restart gate is acknowledged up front),lane_policy_state:=…/demo_lane_policy_state.absent(no persisted SetLanePolicy document can overlay the fixture's targets), andrviz_config:=…/restocker_bringup/rviz/demo_camera_view.rviz(the camera layout above) — and, on the default path, no evidence arguments at all; the ground-truth opt-in additionally passesobject_observation_topic,lane_observation_topic, both tray duties off,product_observation_max_age:=2.0,selection_object_max_age_ms:=2000andplacement_require_column_growth:=false - Wait up to 300 s for
/restock_action_coordinator/statuswithadmission_ready: true - Sweep all six front-lane stations and both back-tray stations before the first transfer
- Restock from current evidence until the front is full or no compatible pair remains, then repeat the scheduled survey cycle
- Leave the GUI up until Ctrl-C or the optional maximum cycle count is reached
Default fixture (sensor_acceptance_products.yaml + sensor_acceptance_lanes.yaml): lanes
lane_01/lane_02/lane_03 each want one product, lane_04–lane_06 want none, and the tray
stocks exactly one can, one small bottle and one large bottle at interleaved poses. The
ground-truth opt-in pairs the dense scenario with dense_restock_lanes.yaml, whose targets
(can 10, small bottle 9, large bottle 6 per lane) are what the shipped front columns can reach
cleanly; the shipped baseline want of six per lane is already satisfied by that overstocked
fixture, so the loop would otherwise idle immediately, and packed-capacity targets drive the last
placements where the depth-derived held count under-counts them.
Rebuild the dense fixture with tools/generate_dense_restock_scenario.py and the default one
with tools/generate_sensor_acceptance_scenario.py if a surveyed layout changes. To drive the
sparse baseline yourself, use just launch-baseline and send a RestockProduct
action explicitly.