This repository contains the ROS workspace for my Bachelor's thesis at the Munich University of Applied Sciences (Hochschule München).
Title: Softwarearchitektur eines autonom fahrenden Modellautos auf Basis von ROS
English: Software Architecture of an Autonomous Driving Model Car Based on ROS
📄 Full thesis (German): Bachelorarbeit.pdf
The MIT RACECAR project had already been set up at HM and extended by several student projects. By the time I started, the codebase had grown organically into a state that made further development difficult:
- Code was scattered across three separate Git repositories — there was no single source of truth. Understanding the full system meant context-switching between repos.
- Existing student code was poorly structured — the PS4 controller had hardcoded "magic numbers", erratic steering behavior, and bypassed the entire multiplexer architecture by publishing directly to the motor driver. The line-following node used a monolithic
racecar_classwith 6 redundant publishers and tangled responsibilities. - No unified control interface — the real car and the simulation used completely different control pipelines. You could not develop and test on the simulation and expect the same behavior on the lab vehicle.
- Simulation did not match reality — the simulated car had 7 individual wheel controllers (the real car has 2 motors driving paired wheels), incorrect steering geometry, and a different steering offset. The multiplexer was started but never actually wired into the simulation pipeline.
The result: students could not effectively use the simulation for development, the code was hard to extend, and adding new features meant duplicating or patching existing broken logic.
Before: Three disconnected repos, a racecar_additions package with tangled dependencies, a racecar_class that bundled unrelated logic, OpenCV headers vendored into the repo instead of linked properly.
After: A single unified racecar-hm workspace. The entire teleoperation stack (PS4 controller, Xbox controller, line detection, line following, mode switch) was extracted from the monolithic racecar_additions and refactored into a clean, modular teleoperation package with its own CMakeLists.txt and package.xml. Each node has a single responsibility and communicates through the proper ROS interfaces.
Key refactoring:
- PS4 controller (
src/teleoperation/src/ps4_controller.cpp): Rewrote from scratch — replaced magic number button indices with named constants, fixed erratic servo behavior, integrated with theackermann_cmd_muxso commands go through the proper arbitration pipeline instead of directly to the motor driver, removed redundant publishers. - Line-following motion node (
src/teleoperation/src/line_detection/motion_node.cpp): Created a proper class-based design with a clean header (motion_node.h), eliminated the dependency on the bloatedracecar_class, reduced 6 publishers down to 1 that correctly publishesAckermannDriveStampedthrough the multiplexer. - Runtime mode switch (
src/teleoperation/src/switch.cpp): Built a new node that lets the driver toggle between manual controller mode and autonomous line-following mode at runtime via the Select/Share button — without this, switching modes required stopping and restarting nodes.
Adapted and extended SoftBank Corp.'s open-source vesc_hw_interface (Apache 2.0) to drive the lab car's VESC through the ros_control framework and close the real-vs-simulation gap. The upstream package exposes a single joint via one command_mode; the work here:
- Registers two interfaces simultaneously — a
PositionJointInterfacefor the steering servo and aVelocityJointInterfacefor the drive motor — so one node controls both axes of the Ackermann car (vesc_hw_interface.cpp). - Adds a servo-mode origin calibration for the steering servo, whose center position cannot be read back, so the interface calibrates it dynamically on startup (
vesc_servo_controller.cpp). - Replaces the upstream PID law with a PT1-style position controller (the original PID is kept, commented, for reference).
- Loads joint limits for both joints from the URDF and the parameter server.
The result is a unified control pipeline: the same ros_control interface drives the real car and the Gazebo simulation.
Overhauled the Gazebo simulation to match the lab vehicle's hardware:
- Reduced from 7 individual wheel controllers down to 3 (one joint state, one velocity, one position controller) — matching the real car's 2-motor, Ackermann-steering layout
- Wheel parallelization via
mimicjoint plugin so two motors drive all four wheels, exactly like the real vehicle - Corrected steering neutral position offset (0.5304 rad) to match the real servo
- Removed the
servo_commandsintermediary node that existed only in simulation - Multiplexer now actively wired into the simulation pipeline (previously started but not connected to anything)
- Fixed PID controller gains in
racecar_control.yamlso the simulation behaves like the real car
- ROS / roscpp — nodes, topics, custom messages, launch files, the parameter server, and
ros_control - C++ — class-based node design and refactoring legacy code into modular, single-responsibility packages
- ros_control hardware interfaces —
hardware_interface,controller_manager, joint limits, and a custom position controller - Computer vision — OpenCV line detection on a ZED stereo-camera feed via
cv_bridge - Robotics simulation — Gazebo, URDF/xacro modeling, mimic-joint plugins, and PID controller tuning
- Hardware integration — VESC motor controller over serial, with automatic servo calibration
- Software architecture — command multiplexing/arbitration, runtime mode switching, and sim-to-real alignment
- Working with legacy code — consolidating three repositories and untangling undocumented student code
- Sim-to-real divergence was the core problem. The simulation and the lab car had drifted into two different control pipelines, so nothing developed in simulation could be trusted on the real vehicle. Converging them onto a single
ros_controlpipeline was the central architectural decision. - The steering servo gives no position feedback. Its electrical center is unknown at startup, so the hardware interface runs an automatic origin calibration before closed-loop control begins.
- Inherited code was opaque. The previous controller used unexplained floating-point literals for every joystick axis and button — working out what each one did was the hardest part of the rewrite, which is why the refactored code leans on named constants and single-responsibility nodes.
| Package | Key Files | What I Did |
|---|---|---|
src/teleoperation/ |
ps4_controller.cpp, xbox_controller.cpp, switch.cpp, line_detection/*, msg/navigation.msg, launch/*, CMakeLists.txt, package.xml |
Complete refactor. Rewrote PS4 controller from magic-numbered mess into clean code with proper multiplexer integration. Created the switch node for runtime mode toggling. Refactored line-following from a monolithic class with 6 publishers into a clean node publishing through the mux. Bundled everything into a new teleoperation package with its own build config. |
src/vesc/vesc_hw_interface/ |
vesc_hw_interface.cpp, vesc_servo_controller.cpp, config/*, launch/* |
Extended SoftBank's Apache-2.0 vesc_hw_interface. Added simultaneous position (steering) + velocity (drive) control, automatic servo-origin calibration, a PT1 position-control law, and all config/launch so the real car shares one ros_control pipeline with the simulation. |
| Simulation fixes | racecar_control.yaml, racecar.xacro, macros.xacro |
PID tuning, steering offset correction, wheel mimic joint parallelization, controller reduction from 7→3. |
This workspace builds upon several open-source projects, each retaining its original license. My original code lives in the teleoperation package. I also extended the third-party vesc_hw_interface (SoftBank Corp., Apache 2.0) and made configuration/URDF changes to racecar_gazebo to align the simulation with the lab car; the remaining MIT/BSD packages are used as-is.
| Package | Origin | License |
|---|---|---|
ackermann_cmd_mux/ |
MIT RACECAR | BSD |
racecar/ |
MIT RACECAR | BSD |
racecar_gazebo/ |
MIT RACECAR Gazebo | BSD |
razor_imu_9dof/ |
KristofRobot/razor_imu_9dof | BSD |
vesc/ (driver packages) |
MIT RACECAR VESC | BSD / Apache 2.0 |
vesc/vesc_hw_interface/ (adapted & extended) |
SoftBank Corp. — Yuki Onishi | Apache 2.0 |
roboticsgroup_gazebo_plugins/ † |
roboticsgroup Gazebo plugins | BSD |
zed-ros-wrapper/ † |
Stereolabs ZED ROS wrapper | Apache 2.0 |
† roboticsgroup_gazebo_plugins/ and zed-ros-wrapper/ are external dependencies, not included in this repository (see .gitignore); clone them into src/ separately.
Required dependencies (install separately):
ros-*-joyros-*-ackermann-msgsros-*-ros-controllerswjwwood/serial(GitHub) — for VESC serial communication
Tested on: Ubuntu 18.04 (ROS Melodic) and Ubuntu 20.04 (ROS Noetic)
# Install ROS dependencies
sudo apt install ros-${ROS_DISTRO}-joy
sudo apt install ros-${ROS_DISTRO}-ackermann-msgs
sudo apt install ros-${ROS_DISTRO}-ros-controllers
# Build the workspace
cd racecar-hm
catkin build
source devel/setup.bashroslaunch racecar_gazebo racecar.launch# PS4 controller (also starts joy_node)
roslaunch teleoperation ps4_controller.launch
# Xbox controller (use_switch routes commands through the manual/autonomous mux)
roslaunch teleoperation xbox_controller.launch use_switch:=trueroslaunch teleoperation line_detection.launchroslaunch racecar_control all.launch ps4_controller:=true line_following:=true world_name:=racecar_lineMy original work — the teleoperation package (src/teleoperation/) — is licensed under the MIT License. See LICENSE.
My changes to src/vesc/vesc_hw_interface/ are modifications of SoftBank Corp.'s package and remain under its original Apache 2.0 license, with the modifications noted in that package's README.
All other third-party packages retain their own licenses as noted above.