Skip to content

Simulation test: drive DiffBot and Remo in Gazebo in CI - #112

Open
fjp wants to merge 6 commits into
noetic-develfrom
feat/simulation-test
Open

fjp wants to merge 6 commits into
noetic-develfrom
feat/simulation-test

Conversation

@fjp

@fjp fjp commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

The first tests that check how the robot behaves, not only that everything builds. Both CI workflows run them on every pull request: CI with industrial_ci, and Dev container.

What the test does

A rostest per robot, diffbot_gazebo/test/sim_drive_diffbot.test and sim_drive_remo.test, starts the robot in Gazebo without a GUI, on the empty world, with the diff drive controller. It needs no display. Two tests then run:

  • publishtest, a test node that comes with rostest: /diffbot/joint_states and /diffbot/mobile_base_controller/odom are published.

  • test_drive.py: drives 0.2 m/s for 3 s, then turns at 0.5 rad/s for 3 s, and checks each move twice:

    • odometry: what the diff drive controller computes from the wheel joints and its configuration;
    • true pose: where Gazebo says the robot is (/gazebo/model_states).

    Both must be in range (forward: 0.4–0.75 m, straight ahead; turn: 0.5–2.0 rad to the left), and they must agree: within 0.05 m forward and 0.25 rad when turning.

    The odometry and Gazebo report poses in different frames (odometry starts at 0, 0, 0, Gazebo spawns the robot at x = -2, y = -0.5). So the test compares motions, not poses. relative_motion() describes the motion between two poses as in the odometry motion model of Probabilistic Robotics (Thrun, Burgard, Fox, section 5.4): a rotation rot1 towards the end position, a translation trans, and a rotation rot2 to the end heading. These don't depend on the frame. Like ROS amcl's odometry model, rot1 is 0 for translations under 1 cm (turning on the spot). The heading comes from tf's euler_from_quaternion. Each method does one step: drive() sends the velocity command, stop() stops the robot, measure_motion() returns the motion from the odometry and from the true pose, and the tests call it with named arguments. The two tests, test_forward and test_turn, each check only their own motion, so their order doesn't matter. The script has type hints and passes flake8 with ROS 2's ament_flake8 settings (PEP 8, line length 99) and mypy.

The true pose is what makes the test useful. The odometry repeats some configuration mistakes, so on its own it would still look right.

What it catches

Each case was checked by breaking diffbot_control.yaml in a copy and running the tests:

Broken Result
Wrong left wheel joint name Controller doesn't load, no odometry: publishtest fails, test_drive errors
Left and right wheels swapped Robot turns right (true pose −1.26 rad) while the odometry reports +1.36 rad: fails (2 failures over both robots)
Wheel radius 20 % too big Odometry 0.58 m forward, true pose 0.49 m: fails (3 failures over both robots)

Changes

  • diffbot_gazebo/test/: sim_drive.launch (Gazebo headless with diffbot_empty.world, controllers without rqt_robot_steering), the two .test files and test_drive.py. They're registered with add_rostest in CMakeLists.txt, with test_depends in package.xml. remo_description can't be one: it isn't a released ROS package, and the dev container's Dockerfile resolves every package.xml before setup.sh clones it. Both workflows clone it with diffbot_dev.repos.
  • Install rules for diffbot_description, diffbot_control and diffbot_gazebo (urdf, config, meshes, launch, rviz, worlds, models). None of the packages installed its files. industrial_ci builds into an install space by default (catkin config --install), where xacro couldn't find diffbot.urdf.xacro. remo_description gets the same rule in Install the model files, so the package works from an install space remo_description#1. The other packages, including diffbot_base's nodes, still have no install rules: Install rules for all packages, so they work from an install space #113.
  • One Gazebo port per test file (GAZEBO_MASTER_URI, 11351 and 11352). catkin runs the two test files in parallel, each with its own ROS master. On Gazebo's default port 11345, the second Gazebo server couldn't start, and Remo's test timed out. The two ports also don't collide with a simulation that's open in the dev container.
  • diffbot_control.launch: a new argument rqt_robot_steering (default true, so nothing changes), so the test can leave out the GUI.
  • diffbot_view.launch: the default world pointed to worlds/db_world.launch, which doesn't exist; it's worlds/db_world.world. diffbot.launch always overrode it, so this only mattered when the file was launched on its own.
  • CI: industrial_ci builds remo_description from source with UPSTREAM_WORKSPACE: diffbot_dev.repos, like the dev container, because it isn't a released ROS package.

Turning on the spot reaches about 1.3 rad for DiffBot and 1.0–1.2 rad for Remo, instead of the commanded 1.5 rad. Gazebo's wheel velocity controller (diffbot_control/config/pid.yaml) has only a P gain, which leaves Remo's wheels at about 80 % of the commanded speed when turning, and the wheels slip. The test therefore only checks that the robot turns left by a clear amount, and that odometry and true pose agree. #116 tunes the gains (measurements there) and then tightens the turn check.

Testing

In the dev container image, with the commands of the Dev container workflow (catkin build --catkin-make-args run_tests, catkin_test_results build):

  • Six clean runs: 10 tests, 0 errors, 0 failures, about 50 s each for both robots in parallel.

  • The three broken cases above fail as described.

  • From an install space, as industrial_ci builds (catkin config --install, with the install rule added to remo_description): 10 tests, 0 errors, 0 failures.

industrial_ci's Remo test needs ros-mobile-robots/remo_description#1 merged first, because industrial_ci clones remo_description's main branch.

Closes #110

A rostest per robot starts it in Gazebo without a GUI (no display
needed), with the diff drive controller, and runs two tests:
- publishtest: joint states and odometry are published
- test_drive.py: 0.2 m/s for 3 s and 0.5 rad/s for 3 s; checks the
  controller's odometry and the robot's true pose from Gazebo
  (/gazebo/model_states), and that the two agree

The true pose catches mistakes the odometry repeats: with the wheel
joints swapped the robot turns right while the odometry reports left,
and a wheel radius 20 % too big makes odometry and true distance differ.
A wrong joint name stops the controller, so no odometry is published.

- diffbot_gazebo/test: sim_drive.launch, sim_drive_diffbot.test,
  sim_drive_remo.test, test_drive.py; registered with add_rostest
- Each test file gets its own Gazebo port (GAZEBO_MASTER_URI): catkin
  runs them in parallel, and two Gazebo servers on the default port made
  the second one fail
- diffbot_control.launch: argument rqt_robot_steering (default true), so
  the test can leave out the GUI
- diffbot_view.launch: the default world pointed to the missing
  worlds/db_world.launch; it's worlds/db_world.world
- CI: industrial_ci builds remo_description from diffbot_dev.repos

Closes #110
…ription test_depend

- diffbot_description, diffbot_control and diffbot_gazebo install their
  files (urdf, config, meshes, launch, rviz, worlds, models).
  industrial_ci builds with an install space only, and xacro couldn't
  find diffbot.urdf.xacro there. remo_description gets the same rule in
  ros-mobile-robots/remo_description#1.
- diffbot_gazebo no longer declares remo_description as a test_depend:
  the dev container's Dockerfile installs the dependencies of every
  package.xml before setup.sh clones remo_description, so rosdep couldn't
  resolve it. Both workflows clone it with diffbot_dev.repos.
"industrial_ci tests from one" left open what "one" is. Say what
the install space is, what goes missing without the rules and who
builds from it.
@fjp
fjp force-pushed the feat/simulation-test branch from 52a9908 to 5dd9f82 Compare October 14, 2026 18:53
fjp added a commit to ros-mobile-robots/remo_description that referenced this pull request Oct 14, 2026
* Install the model files, so the package works from an install space

catkin's install space had only package.xml and the CMake files:
config, launch, meshes, rviz and urdf weren't installed. diffbot's
simulation test (ros-mobile-robots/diffbot#112) builds this package in
industrial_ci, which always uses an install space, and xacro then can't
find urdf/remo.urdf.xacro.

* Explain the install rule's comment more clearly

"industrial_ci tests from one" left open what "one" is. Say what
the install space is, what goes missing without the rule and who
builds from it.
fjp added 2 commits October 14, 2026 23:01
…ering

- test_drive.py: moved() (was change()) explains why the moves are compared
  in the robot's start frame and where the rotation comes from; the yaw comes
  from tf's euler_from_quaternion instead of a hand-written formula; topic
  constants end in _TOPIC; the tests are test_forward and test_turn, since
  they don't depend on each other's order; why cmd_vel is sent repeatedly.
- .test files: what publishtest does, with a link to its documentation.
- diffbot_control.launch: why rqt_robot_steering can be turned off; final
  newline.
relative_motion() (was moved()) describes the motion between two poses as
in the odometry motion model of Probabilistic Robotics (Thrun, Burgard,
Fox, section 5.4): a rotation rot1 towards the end position, a translation
trans, and a rotation rot2 to the end heading. Like ROS amcl, rot1 is 0
for translations under 1 cm. The forward test checks trans and that rot1
and the total turn stay small (straight ahead); the turn test checks the
total turn and trans. Fault checks unchanged: a 20 % too big wheel radius
gives 3 failures, swapped wheels 2.
- relative_motion() takes start_pose and end_pose, typed Pose2D (x, y,
  theta); Motion is a typing.NamedTuple
- drive() only sends the velocity command (end_time instead of end);
  stop() sends zero and waits; measure_motion() remembers both poses,
  calls the two and returns the odometry and true pose motions
- The tests call measure_motion() with named arguments
- odom_pose, true_pose and cmd_vel are declared on the class, with a
  comment why they belong to it
- planar(), wrap() and relative_motion() explain the quaternion, the
  atan2 wrap and hypot
- Type hints; PEP 8 with ROS 2's line length of 99 (flake8 with
  ament_flake8's settings and mypy pass)
- test_turn's comment names the cause of the short turn, Gazebo's
  P-only wheel velocity controller, and the follow-up #116

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

CI: test DiffBot and Remo driving in a Gazebo simulation

1 participant