Understanding the Vehicle Controller

The CPM Lab vehicle controller turns a high-level driving request into low-level actuator commands for a muCar. In the source code it is still called the mid-level controller, because it sits between the high-level planner and the hardware interface on the vehicle.

The controller runs on the muCar itself. The installation and deployment of the vehicle computer are described in muCar, while this page focuses on the controller logic and the command types it accepts.

Controller overview

The controller sits in the middle of the CPM Lab driving stack and connects the planner, the vehicle observation stream, and the hardware interface.

        ---
config:
  layout: elk
  startOnLoad: false

---
flowchart TD
   subgraph Second [Domain ID 21]
      subgraph First [Vehicle]
         direction TD
         MC[Vehicle Controller]
         HW[muCar hardware]
      end
      direction LR
      IPS[IPS]
      TRIG[System trigger]
   end
   subgraph Third [Domain ID 1]
      direction TD
      HL[Motion Planner]
   end
   BR[Bridge]
   HL -->|command| BR
   BR -->|command| MC
   IPS -->|observation| MC
   TRIG -->|trigger| MC
   MC -->|motor_throttle, steering_servo| HW
   MC -->|state| BR
   BR -->|states| HL
    

Communication flow between the vehicle controller, planner, and hardware

It receives commands and observations, selects the active control mode, computes actuator outputs, and publishes the current vehicle state back to the rest of the system.

What the controller does, how it chooses a mode, and what the command types mean

At runtime the controller combines the current vehicle state, a vehicle command from the higher-level software, and system trigger messages such as ready, stop, or LED requests. The state includes pose, speed, IMU values, battery voltage, motor current, and the age of the latest IPS update.

The command type selects the control mode:

  • DIRECT maps to direct actuator control,

  • SPEED_CURVATURE maps to speed and curvature control,

  • PATH_TRACKING maps to geometric path tracking,

  • TRAJECTORY maps to time-parameterized trajectory tracking,

  • INVALID (0) leaves the controller without a valid driving request; the vehicle ignores the command.

Each control cycle produces two actuator outputs:

  • motor_throttle in the range [-1, 1],

  • steering_servo in the range [-1, 1].

The selected mode determines how those outputs are computed. Stop brings the vehicle to a halt and centers the steering, so it also serves as the safety fallback. Direct passes the requested throttle and steering through almost unchanged before clamping them to the valid actuator range. SpeedCurvature interprets the command as a desired speed and curvature, regulates speed with a PI controller, and converts curvature into a steering servo command using a fitted steady-state mapping. PathTracking follows a geometric path by finding the closest point, computing lateral and heading errors, and applying a Stanley-style steering law together with speed regulation. Trajectory uses a time-parameterized trajectory and an MPC-based controller to predict the future vehicle motion and track the reference trajectory.

Those values are then forwarded to the hardware layer that drives the muCar.

How the control loop works

The ROS2 node in mid_level_controller_node.cpp runs a periodic timer at 20 ms. In each cycle it:

        ---
config:
  layout: elk
  startOnLoad: false

---
flowchart TD
   subgraph Second [Main Loop]
      direction TD
      STEP1[1. Update internal vehicle model]
      STEP2[2. Compute control output]
      STEP3[3. Publish current vehicle state]
      STEP4[4. Publish simulated observations]
      style STEP4 stroke-dasharray: 5
   end
   OUT[Actuator outputs]
   OBS[Latest observation]
   CMD[Vehicle command]
   TRIG[System trigger]
   STEP4 -.-> OBS
   linkStyle 0 stroke-dasharray: 5
   OBS --> STEP1
   CMD --> STEP2
   TRIG --> STEP2
   STEP1 --> STEP2 --> STEP3 --> STEP1
   STEP3 --> STEP4
   STEP2 --> OUT
    

Main control loop of muCars

The node subscribes to:

  • /lab/vehicle_<id>/vehicleObservation for localization and sensor updates,

  • /lab/vehicle_<id>/command for driving commands,

  • /lab/system_trigger for ready, stop, and LED control.

It publishes:

  • /lab/vehicle_<id>/vehicleState with the current estimated state,

  • /lab/system_trigger when the vehicle reports that it is ready,

  • /lab/vehicle_<id>/vehicleObservation in simulation mode to emulate IPS updates.

Safety behavior

The controller contains a few safety checks that are important for users:

  • If a stop trigger is received, the vehicle is held in stop mode for 1.0 seconds.

  • If the IPS update age becomes greater than 3.0 seconds while the vehicle is following a PATH_TRACKING or TRAJECTORY command, the controller switches to stop mode, clears the active command, and holds the stop state for 1.0 seconds.

  • All actuator outputs are clamped to the valid range [-1.0, 1.0] before they are sent to the hardware layer.

This means that the controller is designed to fail safe: when localization is unreliable or a stop request arrives, it prefers to stop rather than continue driving.

Simulation mode

The same node can also run in simulation mode. In that case it initializes a simulated IPS source and can publish synthetic vehicle observations. This makes it possible to test the controller logic without a physical muCar, while still using the same ROS2 interfaces.