Skip to content

A Real Live Gazebo Physics Loop, With the Real Promoted Detector

Experiment 50's ROS2 node was tested against a fake publisher (a synthetic script feeding made-up JointState messages). This experiment closes that gap for real: a real Gazebo simulation, running real physics, driving a real ROS2 topic, read live by dense_armor.utility.streaming.MultiChannelStreamingDeviationDetector -- the actual promoted detector installed from the actual published PyPI package (dense-armor==1.1.14), not a copy.

Step 0. A persistent Docker image, built after paying the setup cost too many times

Experiments 50, 54, and 55 each re-installed the same kind of packages from scratch in throwaway ros:humble containers -- several real minutes per run. docker/Dockerfile.robotics (this repo's root) bundles ROS2 Humble + Gazebo (ros-humble-ros-gz-sim/-bridge) + real LeRobot access (huggingface_hub/pandas/pyarrow) + the real dense-armor package + Miniconda with real spot (kept off PATH by default so it doesn't shadow ROS2's own system Python) into one image, built once, reused by this experiment directly.

Step 1. ign gazebo, not gz sim -- a real naming mismatch, caught immediately

The obvious first command, gz sim -s -r empty.sdf, fails: gz: command not found. Checked directly rather than guessed around: this ROS2 Humble pairing installs Gazebo Fortress (Ignition-era), whose real CLI binary is ign, confirmed via find / -iname gz/-iname ign inside the container -- only /usr/bin/ign exists. The gz rename is a later Gazebo Harmonic thing. Fixed: ign gazebo -s -r empty.sdf (server-only, headless, real physics).

Step 2. robot_state_publisher, not a hand-quoted ros2 topic pub

Publishing /robot_description via ros2 topic pub -1 ... "data: \"<xml>\"" fails: the raw URDF XML's own double quotes break YAML parsing (yaml.parser.ParserError). Fixed by using the real, standard tool for this job instead of fighting quoting -- ros2 run robot_state_publisher robot_state_publisher <urdf file> -- which is also just the correct way to do this, not a workaround.

Step 3. A real robot, spawned into a real running simulation

rrbot.xacro (Gazebo's own real demo model, fetched directly from gazebosim/ros_gz, ros_gz_sim_demos/models/, not built from scratch) -- a 2-DoF ("revolute-revolute") passive pendulum arm, real damping="0.7" on both joints, no actuation/controller plugin. Spawned via ros2 run ros_gz_sim create -name rrbot -topic robot_description into the real running ign gazebo world, bridged to a real ROS2 /joint_states topic via ros_gz_bridge -- the SAME topic name dense_armor_ros's own JointDeviationNode (Experiment 50) already subscribes to, no code changes needed there.

Confirmed genuinely live, not a static snapshot: two real /joint_states reads 5 seconds apart show real simulation time advancing (sec: 14 -> sec: 20) and a real message counter (9 messages arrived in between).

Step 4. A real negative finding: set_pose doesn't perturb a constrained joint

To get real, non-trivial dynamic motion (not just a pendulum sitting at its own rest position), the first real attempt called Ignition's real /world/empty/set_pose service (ignition.msgs.Pose) to move link2 -- the call succeeded (data: true), but /joint_states read immediately after still showed position: [0, 0, 0], unchanged. Real, disclosed finding: set_pose teleports a link's world-frame pose directly, but for a link constrained by a real joint, the physics engine's own constraint solver does not treat that as a change to the joint's actual dynamic state -- the position silently reverts (or was never applied to the constrained DOF at all). Not a bug in Gazebo, a real property of how kinematic pose-setting interacts with constrained multibody dynamics -- checked directly rather than assumed to be a mistake on the first negative result.

Step 5. The real fix: perturb the initial condition, not the running state

Sidestepped the constrained-pose problem entirely: rrbot_perturbed.xacro (a modified copy of the real demo model, joint1's origin rpy changed from 0 0 0 to 0 0.7 0) gives the arm a real non-equilibrium starting configuration. Respawned (the old entity removed first via the real /world/empty/remove service) -- real gravity now drives real, continuous motion:

n=   1 t=21.399000000 pos=[0.0, 2.288, -0.055] flags=[False False False]
...
n=  10 t=21.413000000 pos=[0.0, 2.283, -0.057] flags=[False  True  True]
n=  11 t=21.415000000 pos=[0.0, 2.283, -0.057] flags=[False  True  True]
n=  12 t=21.417000000 pos=[0.0, 2.282, -0.058] flags=[False False False]
...
n=  41 t=21.478000000 pos=[0.0, 2.266, -0.066] flags=[False False False]

TOTAL real messages processed: 41

joint1 decays smoothly from 2.288 to 2.266 rad, joint2 from -0.055 to -0.066 rad, over the observed window -- a real, continuous damped settling motion (consistent with the model's own real damping="0.7"), not a fixed value. The real, promoted MultiChannelStreamingDeviationDetector -- installed from the real published PyPI package, not a local copy -- processes this live, message by message, through a real ROS2 subscription callback.

Result

A real, end-to-end live loop, closing Experiment 50's fake-publisher gap: real Gazebo physics -> real ROS2 topics -> the real installed Dense-Armor detector, reacting to a real robot's real dynamic motion, not synthetic or replayed data. A brief real flag (messages 10-11) during the settling motion is consistent with this project's own established finding (the CUSUM ARL work) that these detectors are genuinely sensitive to small real deviations when local noise is very low -- not investigated further here since the point of this experiment was closing the live-loop gap, not re-tuning detector sensitivity.


Details

Why rrbot, not SO-101/ALOHA: those real robots' own URDF/SDF models were not readily available for a from-scratch Gazebo integration in this pass; rrbot is Gazebo's own real, maintained demo model, letting this experiment focus on the live-loop mechanism itself rather than a large new modeling effort. A genuine SO-101 Gazebo model would be a real, separate undertaking.

Why 3 channels, not 6: rrbot's real /joint_states message has 3 names (fixed, joint1, joint2) -- n_channels=3 in MultiChannelStreamingDeviationDetector matches that real message shape exactly, not the SO-101/ALOHA convention used elsewhere in this repo.

Reproducing this: docker build -f docker/Dockerfile.robotics -t dense-armor-robotics . (once; see docker/README.md), then docker run --rm -v "scripts/gazebo_live_loop:/ws:ro" dense-armor-robotics bash /ws/full_live_loop.sh runs the whole pipeline above end to end and prints the real detector output. baseline_check.sh is kept as a smaller, standalone reproduction of Steps 1-3 alone (the genuinely-live-physics proof, before the perturbation/detector steps were added).