Skip to content

Add Volkswagen carcontroller and values modules (MQB/PQ platform support) - #9

Open
Terges wants to merge 2 commits into
tealtwo:landingfrom
Terges:landing
Open

Add Volkswagen carcontroller and values modules (MQB/PQ platform support)#9
Terges wants to merge 2 commits into
tealtwo:landingfrom
Terges:landing

Conversation

@Terges

@Terges Terges commented Jul 5, 2026

Copy link
Copy Markdown

Add a Volkswagen-specific CarController implementation and a values module to provide CAN definitions, platform/car specs, and firmware-query configuration for MQB and PQ Volkswagen group vehicles.

Implements lateral (HCA) steering control, longitudinal (ACC) control and HUD messaging, stock ACC button emulation, cruise-button automation logic, and fuzzy FW-to-car matching for VIN/platform detection.

What changed

new file: selfdrive/car/volkswagen/carcontroller.py
Implements CarController class for Volkswagen vehicles:
Steering logic with EPS timer mitigation, torque limits, and HCA enable/disable handling.
Acceleration/ACC control message generation for openpilot longitudinal control.
HUD (LKA/ACC) message creation and lead display handling.
Stock ACC button control and automated button-press logic (multi-mode handling for different ACC behaviors).
Helper methods for curve/turn speed, speed-limit integration, and button-press state machine.
Integrates different CAN layouts for MQB / PQ / MLB using flags.

new file: selfdrive/car/volkswagen/values.py
Defines CarControllerParams (steer/ACC timing, limits, message steps) and CANBUS constants.
Adds Volkswagen platform/config classes (MQB/PQ), VolkswagenCarSpecs, CAR enum entries for many VW/Skoda/Seat/Audi models, and documentation/footnotes.
Provides FW query configuration and a fuzzy match algorithm to map firmware responses & VIN to supported cars.
Supplies button mappings, LDW/ACC HUD codes, DBC handling, and FW-query constants.

Why
Adds comprehensive Volkswagen platform support including both MQB and PQ families, encapsulating vehicle-specific CAN message generation and safety mitigations.
Provides infrastructure for identifying VW group cars by firmware/VIN and for safely controlling steering, ACC and HUD on these platforms.

Summary by Sourcery

Add Volkswagen-specific car controller and platform definitions to support steering, longitudinal control, HUD, and firmware-based identification for MQB and PQ vehicles.

New Features:

  • Introduce a Volkswagen CarController implementation with steering, longitudinal, HUD, and ACC button handling logic for MQB, PQ, and MLB platforms.
  • Add Volkswagen values module defining controller parameters, CAN bus/layout constants, platform and car specs, VIN/WMI-based platform mapping, and supported VW-group models.
  • Implement fuzzy firmware-to-car matching and firmware query configuration to identify Volkswagen group vehicles via ECU responses and VIN metadata.

Enhancements:

  • Provide stock ACC button emulation and automation logic integrating speed limit and curve speed control for vehicles without pcmCruiseSpeed.

Terges added 2 commits July 5, 2026 11:51
…M, STEER_LOW_TORQUE, STEER_TIME_LOW_TORQUE, STEER_TIME_RESET)
…CA-disabled frame' workaround to PQ and MLB racks; update CarController to select CCS by flags and send dual steering outputs where needed
@sourcery-ai

sourcery-ai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduces Volkswagen MQB/PQ platform support by adding a VW-specific CarController that handles steering, longitudinal ACC, HUD, and stock ACC button emulation, plus a values module that defines platform configs, car specs, CAN layout, firmware query behavior, and fuzzy VIN/FW matching for a wide range of VW-group models.

Sequence diagram for Volkswagen CarController update cycle

sequenceDiagram
  participant CarController
  participant SubMaster_longPlan as SubMaster_longitudinalPlanSP
  participant CCS as Volkswagen_CAN_Signals
  participant CANPacker_pt as CANPacker_pt
  participant VW_ECU as Volkswagen_PT_CAN

  CarController->>SubMaster_longPlan: update(0)
  alt openpilotLongitudinalDisabled
    SubMaster_longPlan-->>CarController: longitudinalPlanSP (v_tsc_state, slc_state, m_tsc_state, speed_limit)
  end

  loop every_STEER_STEP
    CarController->>CarController: apply_driver_steer_torque_limits()
    CarController->>CarController: EPS_timer_mitigation_logic
    CarController->>CCS: create_steering_control(packer_pt, CANBUS_pt, apply_steer, hca_enabled)
    CCS-->>CarController: steer_msg
    CarController->>VW_ECU: send steer_msg
    opt STOCK_HCA_PRESENT
      CarController->>CCS: create_eps_update(packer_pt, CANBUS_cam, eps_stock_values, ea_simulated_torque)
      CCS-->>CarController: eps_update_msg
      CarController->>VW_ECU: send eps_update_msg
    end
  end

  loop every_ACC_CONTROL_STEP when openpilotLongitudinalControl
    CarController->>CCS: acc_control_value(cruise_available, accFaulted, longActive)
    CCS-->>CarController: acc_control
    CarController->>CCS: create_acc_accel_control(packer_pt, CANBUS_pt, acc_type, longActive, accel, acc_control, stopping, starting, esp_hold_confirmation)
    CCS-->>CarController: acc_accel_msg
    CarController->>VW_ECU: send acc_accel_msg
  end

  loop LDW_STEP and ACC_HUD_STEP
    CarController->>CCS: create_lka_hud_control(...)
    CCS-->>CarController: lka_hud_msg
    CarController->>VW_ECU: send lka_hud_msg

    CarController->>CCS: acc_hud_status_value(...)
    CCS-->>CarController: acc_hud_status
    CarController->>CCS: create_acc_hud_control(...)
    CCS-->>CarController: acc_hud_msg
    CarController->>VW_ECU: send acc_hud_msg
  end

  alt stock_pcc_buttons
    CarController->>CCS: create_acc_buttons_control(... cancel/resume ...)
    CCS-->>CarController: acc_button_msg
    CarController->>VW_ECU: send acc_button_msg
  else openpilot_button_emulation
    CarController->>CarController: get_cruise_buttons(CS, vCruise)
    CarController->>CCS: create_acc_buttons_control(... buttons, custom_stock_long=True)
    CCS-->>CarController: acc_button_msg_emu
    CarController->>VW_ECU: send acc_button_msg_emu
  end
Loading

File-Level Changes

Change Details Files
Add Volkswagen-specific controller parameters, platform/config definitions, and VIN/FW-based car identification for MQB and PQ platforms.
  • Define CarControllerParams with platform-dependent steering/ACC timing, torque limits, EPS timer mitigation thresholds, and HUD/LDW message codes for MQB vs PQ.
  • Introduce CANBUS constants and WMI/flag enums (VolkswagenFlags, VolkswagenFlagsSP) to distinguish platforms and feature capabilities.
  • Add VolkswagenMQBPlatformConfig and VolkswagenPQPlatformConfig (with PQ flagging) plus VolkswagenCarSpecs with sensible defaults for steering geometry and min steer speed.
  • Define VWCarDocs and Footnote enums to centralize documentation strings, hardware requirements, and footnotes for VW-group vehicles.
  • Populate CAR enum with MQB/PQ configs for many VW/Skoda/Seat/Audi models, including docs, chassis codes, WMIs, masses, wheelbases, and platform flags.
  • Implement match_fw_to_car_fuzzy to select candidate platforms based on fuzzy ECU firmware matches plus VIN WMI and chassis code filtering.
  • Configure FW_QUERY_CONFIG with VW UDS multi-identifier requests, ECU whitelists, RX offsets, extra camera ECU, non-essential EPS ECU, and hook match_fw_to_car_fuzzy.
  • Generate DBC mapping for all VW CAR entries via CAR.create_dbc_map().
selfdrive/car/volkswagen/values.py
Implement Volkswagen CarController handling steering, EPS timer mitigation, ACC actuation, HUD messaging, and stock ACC button automation for MQB/PQ/MLB.
  • Instantiate CarControllerParams, select the appropriate CAN sender module (mqbcan/pqcan/mlbcan) based on CP.flags, and configure CAN packer and external bus selection.
  • Implement HCA steering control with driver torque limits, EPS uninterrupted-steer timer mitigation (low-torque disable, reset window), same-torque watchdog, and Emergency Assist torque spoofing when stock HCA is present.
  • Add longitudinal ACC control path that sends ACC accel/stop/start commands when openpilotLongitudinalControl is enabled, including ESP hold/start handling and clamped accel values.
  • Drive LDW/LKA HUD and ACC HUD messages, mapping openpilot alerts and lead detection into platform-specific HUD signals and cadence (LDW_STEP, ACC_HUD_STEP).
  • Implement stock ACC button passthrough for cancel/resume and automatic button generation when using openpilot longitudinal (custom_stock_long), including detection of vehicle ACC button behavior (incremental vs set/resume semantics).
  • Integrate with longitudinalPlanSP messaging to compute speed-limit and curve-speed-aware target speeds (vision and map-based TSC), with hysteresis, minimum cruise speed handling, and a state machine for when to adjust set speed.
  • Provide a cruise button state machine (get_cruise_buttons* helpers) that translates desired speed adjustments into sequences of accel/decel/set/resume button presses with timing, debouncing, and mode-specific behavior.
  • Return updated actuator values reflecting applied steering torque while emitting the accumulated CAN sends each control cycle.
selfdrive/car/volkswagen/carcontroller.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues, and left some high level feedback:

  • CarController.init assigns self.CCS twice and references VolkswagenFlags.MLB which doesn’t exist in VolkswagenFlags, so the MLB branch is currently dead/buggy and the initial assignment is redundant—clean this logic up and ensure flags align with the values module.
  • In update(), new_actuators.steer is set twice in succession (first from apply_steer_last, then immediately from output_steer), which effectively discards the first value; clarify which value should be exposed and remove the redundant assignment.
  • The calls to create_acc_buttons_control use different parameter patterns (one with self.ext_bus and one without a bus argument), which is likely inconsistent with the helper’s expected signature; standardize these calls to avoid sending ACC button commands on the wrong bus or with incorrect parameters.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- CarController.__init__ assigns self.CCS twice and references VolkswagenFlags.MLB which doesn’t exist in VolkswagenFlags, so the MLB branch is currently dead/buggy and the initial assignment is redundant—clean this logic up and ensure flags align with the values module.
- In update(), new_actuators.steer is set twice in succession (first from apply_steer_last, then immediately from output_steer), which effectively discards the first value; clarify which value should be exposed and remove the redundant assignment.
- The calls to create_acc_buttons_control use different parameter patterns (one with self.ext_bus and one without a bus argument), which is likely inconsistent with the helper’s expected signature; standardize these calls to avoid sending ACC button commands on the wrong bus or with incorrect parameters.

## Individual Comments

### Comment 1
<location path="selfdrive/car/volkswagen/carcontroller.py" line_range="157-160" />
<code_context>
+          self.hca_frame_timer_running = 0
+          apply_steer = 0
+
+      self.eps_timer_soft_disable_alert = self.hca_frame_timer_running > self.CCP.STEER_TIME_ALERT / DT_CTRL
+      self.apply_steer_last = apply_steer
+      can_sends.append(self.CCS.create_steering_control(self.packer_pt, CANBUS.pt, apply_steer, hca_enabled))
+      can_sends.append(self.CCS.create_steering_control(self.packer_pt, CANBUS.pt, output_steer, hca_enabled))
+
+      if self.CP.flags & VolkswagenFlags.STOCK_HCA_PRESENT:
</code_context>
<issue_to_address>
**issue (bug_risk):** Steering command is sent twice per cycle with different torque values on the same bus, which is likely unintended.

Within a single `STEER_STEP`, `create_steering_control` is called twice on `CANBUS.pt`: once with `apply_steer` and once with `output_steer`. Because `output_steer` is usually equal to `apply_steer`, this sends duplicate frames; when they differ (due to mitigation logic), the second call overrides the first. To avoid confusion and unintended behavior, consider emitting a single steering control frame per cycle using the final torque value after the enable/timer logic has selected it.
</issue_to_address>

### Comment 2
<location path="selfdrive/car/volkswagen/carcontroller.py" line_range="228-231" />
<code_context>
+          self.send_count = 0
+        self.last_cruise_button = self.cruise_button
+
+    new_actuators = actuators.as_builder()
+    new_actuators.steer = self.apply_steer_last / self.CCP.STEER_MAX
+    new_actuators.steer = output_steer / self.CCP.STEER_MAX
+    new_actuators.steerOutputCan = self.apply_steer_last
+
+    self.gra_acc_counter_last = CS.gra_stock_values["COUNTER"]
</code_context>
<issue_to_address>
**issue (bug_risk):** new_actuators.steer is assigned twice, with the first assignment immediately overwritten.

`new_actuators.steer` is first set from `self.apply_steer_last` then immediately overwritten by `output_steer`, so the initial value is never used. Please decide which value should populate `steer` and remove the redundant assignment, or introduce a separate field if you need to expose both the commanded and mitigated values.
</issue_to_address>

### Comment 3
<location path="selfdrive/car/volkswagen/carcontroller.py" line_range="100" />
<code_context>
+        self.param_s.put_bool_nonblocking("LastSpeedLimitSignTap", False)
+      self.last_speed_limit_sign_tap_prev = CS.params_list.last_speed_limit_sign_tap
+
+      sl_force_active = CS.params_list.speed_limit_control_enabled and (self.frame < (self.sl_force_active_timer * DT_CTRL + 2.0))
+      sl_inactive = not sl_force_active and (not CS.params_list.speed_limit_control_enabled or (True if self.slc_state == 0 else False))
+      sl_temp_inactive = not sl_force_active and (CS.params_list.speed_limit_control_enabled and (True if self.slc_state == 1 else False))
</code_context>
<issue_to_address>
**issue (bug_risk):** Speed-limit force-active timeout mixes frame counts and seconds, likely causing an incorrect duration.

`self.sl_force_active_timer` is a frame index, but the condition compares `self.frame` (frames) to `self.sl_force_active_timer * DT_CTRL + 2.0` (seconds). This mixes units, so the force-active duration is scaled incorrectly by 1/DT_CTRL. If the goal is a 2-second window after the tap, the comparison should use consistent time units, e.g. `(self.frame * DT_CTRL) < (self.sl_force_active_timer * DT_CTRL + 2.0)` or `self.frame < self.sl_force_active_timer + 2.0 / DT_CTRL`.
</issue_to_address>

### Comment 4
<location path="selfdrive/car/volkswagen/carcontroller.py" line_range="220-221" />
<code_context>
+            self.cruise_button = 1 if self.cruise_button == 1 else 2  # accel, decel
+          elif self.acc_type == 1:
+            self.cruise_button = 3 if self.cruise_button == 1 else 4  # resume, set
+          if self.frame % self.CCP.BTN_STEP == 0:
+            can_sends.append(self.CCS.create_acc_buttons_control(self.packer_pt, CS.gra_stock_values, frame=(self.frame // self.CCP.BTN_STEP),
+                                                                 buttons=self.cruise_button, custom_stock_long=True))
+            self.send_count += 1
</code_context>
<issue_to_address>
**issue (bug_risk):** create_acc_buttons_control is called with a different signature than in the earlier branch, which may not match the helper's API.

In the cancel/resume path above, `create_acc_buttons_control` is called as `create_acc_buttons_control(self.packer_pt, self.ext_bus, CS.gra_stock_values, cancel=..., resume=...)`, but here it is `create_acc_buttons_control(self.packer_pt, CS.gra_stock_values, frame=..., buttons=..., custom_stock_long=True)` with no bus argument and shifted positionals. Unless the helper is overloaded for both forms, this will either misassign the bus value or raise a runtime error. Please make the bus argument explicit and align positional/keyword usage with the other call site.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +157 to +160
self.eps_timer_soft_disable_alert = self.hca_frame_timer_running > self.CCP.STEER_TIME_ALERT / DT_CTRL
self.apply_steer_last = apply_steer
can_sends.append(self.CCS.create_steering_control(self.packer_pt, CANBUS.pt, apply_steer, hca_enabled))
can_sends.append(self.CCS.create_steering_control(self.packer_pt, CANBUS.pt, output_steer, hca_enabled))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Steering command is sent twice per cycle with different torque values on the same bus, which is likely unintended.

Within a single STEER_STEP, create_steering_control is called twice on CANBUS.pt: once with apply_steer and once with output_steer. Because output_steer is usually equal to apply_steer, this sends duplicate frames; when they differ (due to mitigation logic), the second call overrides the first. To avoid confusion and unintended behavior, consider emitting a single steering control frame per cycle using the final torque value after the enable/timer logic has selected it.

Comment on lines +228 to +231
new_actuators = actuators.as_builder()
new_actuators.steer = self.apply_steer_last / self.CCP.STEER_MAX
new_actuators.steer = output_steer / self.CCP.STEER_MAX
new_actuators.steerOutputCan = self.apply_steer_last

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): new_actuators.steer is assigned twice, with the first assignment immediately overwritten.

new_actuators.steer is first set from self.apply_steer_last then immediately overwritten by output_steer, so the initial value is never used. Please decide which value should populate steer and remove the redundant assignment, or introduce a separate field if you need to expose both the commanded and mitigated values.

self.param_s.put_bool_nonblocking("LastSpeedLimitSignTap", False)
self.last_speed_limit_sign_tap_prev = CS.params_list.last_speed_limit_sign_tap

sl_force_active = CS.params_list.speed_limit_control_enabled and (self.frame < (self.sl_force_active_timer * DT_CTRL + 2.0))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Speed-limit force-active timeout mixes frame counts and seconds, likely causing an incorrect duration.

self.sl_force_active_timer is a frame index, but the condition compares self.frame (frames) to self.sl_force_active_timer * DT_CTRL + 2.0 (seconds). This mixes units, so the force-active duration is scaled incorrectly by 1/DT_CTRL. If the goal is a 2-second window after the tap, the comparison should use consistent time units, e.g. (self.frame * DT_CTRL) < (self.sl_force_active_timer * DT_CTRL + 2.0) or self.frame < self.sl_force_active_timer + 2.0 / DT_CTRL.

Comment on lines +220 to +221
if self.frame % self.CCP.BTN_STEP == 0:
can_sends.append(self.CCS.create_acc_buttons_control(self.packer_pt, CS.gra_stock_values, frame=(self.frame // self.CCP.BTN_STEP),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): create_acc_buttons_control is called with a different signature than in the earlier branch, which may not match the helper's API.

In the cancel/resume path above, create_acc_buttons_control is called as create_acc_buttons_control(self.packer_pt, self.ext_bus, CS.gra_stock_values, cancel=..., resume=...), but here it is create_acc_buttons_control(self.packer_pt, CS.gra_stock_values, frame=..., buttons=..., custom_stock_long=True) with no bus argument and shifted positionals. Unless the helper is overloaded for both forms, this will either misassign the bus value or raise a runtime error. Please make the bus argument explicit and align positional/keyword usage with the other call site.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant