Mobi is a differential-drive service robot I designed and built for indoor spaces. It maps a room, localizes, navigates through tight passages, backs out of its dock on its own, and acts on spoken commands such as "undock" or "go to the kitchen". A Raspberry Pi runs the ROS 2 stack; a Teensy 4.1 runs the motor control; the same packages drive a Gazebo twin.
The torso carries a lifting rail and a pan-tilt head, built for arms that were planned but not added. Navigation, voice control and docking run on the real robot.
Voice-driven service demo |
SLAM and navigation |
Robot Launcher GUI |
Playlist: ROS 2 Projects
Case study: Mobi on my portfolio
Also: joystick teleoperation · face and hand tracking driving the head in Gazebo
Build log on LinkedIn (2025): physical structure · drive base progress · encoder readings at speed · joystick control · SLAM map distortion · autonomous chassis · A* vs Dijkstra · Robot Launcher app · adding the torso · voice docking with an LLM
Building Mobi from CAD to simulation (4-part series): Fusion 360 CAD · URDF part 1 · URDF part 2 · spawning in Gazebo
- Mapping and navigation — slam_toolbox, AMCL, Nav2 (Smac Planner 2D + Regulated Pure Pursuit) with replanning and recovery behaviour trees.
- Own motor control stack — Teensy 4.1 firmware with per-wheel PID at 100 Hz and a C++
ros2_controlhardware interface over 500 kbaud serial. - Voice → action — Whisper transcribes, Gemini returns an intent (
dock,undock,go_to_kitchen,go_to_user,stop), and a task handler turns it into Nav2 goals or anUndockaction. - Undocking action server — reverses until the ultrasonic sensor clears 0.35 m, then turns away using IMU yaw rate; cancellable, with timeouts.
- Safety by arbitration — a LiDAR safety-stop node holds a
twist_muxlock at priority 255 above joystick, keyboard and Nav2. - Robot Launcher — a PyQt5 app that starts the real robot or the simulation with map, world, SLAM mode and serial port chosen from drop-downs, and saves versioned maps.
- Own planner and controller plugins — A*, Dijkstra, PD and pure-pursuit written as Nav2 plugins in C++ (available, not enabled by default).
| Compute | Raspberry Pi 4B (8 GB) + 7-inch touchscreen |
| Motion | Teensy 4.1, 2 × DC gear motors with high-resolution quadrature encoders, PWM/DIR driver |
| Sensing | RPLiDAR, MPU6050 IMU, ultrasonic sensor, camera |
| Body | Torso on dual lead-screw linear rails, pan-tilt head; 12 V 30 A supply with 5 V 20 A buck during development |
ROS 2 Humble · Nav2 · slam_toolbox · AMCL · ros2_control · twist_mux · Gazebo Fortress · OpenCV (ArUco) · MediaPipe · Whisper · Gemini API · ElevenLabs · PyQt5 · C++ · Python
mobi_description URDF and worlds · mobi_firmware Teensy sketch + hardware interface · mobi_controller diff-drive, joystick, twist_mux · mobi_mapping / mobi_localization / mobi_navigation SLAM, AMCL, Nav2 · mobi_planning / mobi_motion own Nav2 plugins · mobi_script task handler + undock action · mobi_speech voice · mobi_vision ArUco, face and hand tracking · mobi_utils safety stop · mobi_gui Robot Launcher · mobi_bringup launch files
mkdir -p ~/mobi_ws/src && cd ~/mobi_ws/src
git clone https://github.com/Curious-Utkarsh/mobi-service-robot.git .
cd ~/mobi_ws && rosdep install --from-paths src --ignore-src -r -y
colcon build --symlink-install && source install/setup.bash
ros2 launch mobi_bringup simulated_robot.launch.py world_name:=small_house map_name:=small_house # simulation
ros2 launch mobi_bringup real_robot.launch.py serial_port:=/dev/ttyUSB0 map_name:=Room1 # real robot
ros2 topic pub --once /mobi/intents std_msgs/msg/String "data: 'undock'" # test a behaviourThe voice node reads its keys from the environment: export GEMINI_API_KEY=... and export ELEVENLABS_API_KEY=... before launching it.
- Encoders the ESP32 could not keep up with. With an ESP32 at 240 MHz, wheel counts from the high-resolution encoders were unreliable, which corrupted odometry and everything above it. I isolated the encoder layer, confirmed counts were being missed, and moved motor control to a Teensy 4.1 at 600 MHz, which reads them cleanly.
- Tight passages. With a 0.58 × 0.56 m costmap footprint the robot had to pass ~0.8 m openings. On the real robot it stopped mid-path even with obstacles far away; I traced it to costmaps not clearing after dynamic obstacles moved. The current workaround disables obstacle checks in the local controller — a stopgap, not a fix; a behaviour-tree recovery is next.
- MechaBot — the smaller robot I built next, with QR + LiDAR docking
- Aura — simulated predecessor with an arm and vision docking
- Bumperbot — an earlier differential-drive robot on real hardware
- TurtleBot3 ArUco docking — where the docking work started
Package layout and the mobi_planning / mobi_motion plugins follow Antonio Brandi's Self-Driving and ROS 2 — Learn by Doing course series. House and warehouse worlds use AWS RoboMaker models.