← Back to Research
2025
ROS2 Jazzy Gazebo BLE IMU Collision Detection Python

A ROS 2 Jazzy + Gazebo simulation of a 3-DOF gantry robot racing along a track, driven entirely by live human motion — a pair of wireless torso IMUs, or the potentiometer-based TES rig — mapped in real time to the robot's X, Y, and rotational joints.

Technical Highlights

The simulated robot — joint_controller_demo — is a 3-DOF gantry riding a rail: two prismatic joints (x_axis_joint, y_axis_joint) for forward/back and lateral travel, and one revolute joint (rotational_joint) for yaw. Each joint is driven by a std_msgs/Float64 velocity command published on its own topic, which ros_gz_bridge relays into Gazebo. A POV camera and a top-down camera are mounted on the robot and bridged back out to ROS 2 as image topics, so the sim can be watched from the robot's own perspective as well as from above.

What actually drives those three topics is interchangeable. The primary input is a pair of Bluetooth IMUs strapped to a rider's torso: imu_control.py connects over BLE, reads roll/pitch/yaw from notification packets, and publishes them — scaled — directly as the robot's X/Y/rotation commands, so the rider's own body orientation steers the gantry. A second, earlier hardware revision of the same idea reads three potentiometer values off an Arduino over USB serial instead of BLE. Keyboard teleop and a scripted joint sweep round out the input options, for testing without any hardware attached at all. Since every source publishes to the same three topics, only one runs at a time.

three_dof_robot/
├── launch/test.launch.py         # Main entry point: Gazebo + bridge + control nodes
├── model/
│   ├── bot/robot.sdf              # 3-DOF robot model (joints, cameras, contact sensor)
│   └── dynamic_object/*.sdf       # Moving obstacle models
├── worlds/
│   ├── test.world                 # Track world used by the default launch file
│   └── obstacle_course.world      # Alternate obstacle-course world
├── parameters/bridge_parameters.yaml  # ros_gz_bridge topic map (ROS <-> Gazebo)
└── src/
    ├── imu_control.py             # BLE IMU input (primary control source)
    ├── old_TES_serial_control.py  # Serial potentiometer input (legacy TES hardware)
    ├── keyboard_joint_control.py  # Manual teleop
    ├── test_joint_controllers.py  # Scripted joint sweep test
    ├── collision_handler_node.py  # Collision detection + e-stop
    ├── rotor_position.py          # Pose republisher
    ├── sector_timing.py           # Lap/sector timing + CSV logging
    ├── dynamic_obstacles_controller.py  # Moving obstacle driver
    └── globals.py                 # Shared sector/collision state, CSV path config

Package layout — reproduced from the repo's own README.

The track itself isn't static: three obstacles drift back and forth across it on a sine-wave trajectory, one phase-shifted 180° from the other two. A contact sensor on the robot feeds a dedicated collision node, which zeros all three joint velocities the instant it detects a hit — stopping the robot outright — and tallies collisions per track sector, with a cooldown so a single contact isn't counted twice. Track progress is measured the same way: the course is split into four sectors by track-position thresholds, and a timing node times how long the robot spends in each one, writing sector times and collision counts to a CSV once the robot crosses into the finish sector.

A few rough edges are still open, by design rather than oversight: joint-command scaling is a fixed divide-by-100 in both input scripts rather than a runtime-adjustable gain; the BLE IMU addresses and the CSV output paths are hard-coded to the specific hardware and machine this was developed on; and the sector-boundary thresholds are duplicated between the collision node and the timing node rather than defined once, so both need updating together if the track geometry changes. None of these block the simulation from running — they're the known list of what to clean up next.