7 Commits

Author SHA1 Message Date
brianhansonxyz 37e095eab7 controlsd: fix park short-circuit lac_log type mismatch crashing controlsd
The park short-circuit added in cea422b created lac_log as
LateralDebugState. publish_logs assigns lac_log to
controlsState.lateralControlState.<subtype> based on
CP.lateralTuning.which() — for torque-tuned cars (Hyundai Tucson here)
that's torqueState, expecting LateralTorqueState. The mismatch threw
a capnp KjException ("Value type mismatch") on the first state_control
cycle in park, killing controlsd. Manager respawned it, it crashed
again on the next park cycle, repeat — controlsState was never reliably
published, so the UI never saw the started/onroad transition and didn't
flip to the camera view.

Fix: call self.LaC.update(False, ...) which internally early-returns
when active is False (cheap) but produces the correct lac_log subtype
for whichever controller this car uses (LateralTorqueState for torque,
LateralPIDState for pid, LateralAngleState for angle). Pass through
the same sm reads the regular path uses; they're cached so the cost is
negligible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:43:08 -05:00
brianhansonxyz 5d76576a15 sessions: document hyundai canfd steer torque bump
Reference for the 270->324 change in commit 4058269. Covers what each
constant means, the two-files-must-stay-in-lockstep rule, the path to
push higher (324 -> 384, the community-consensus safe ceiling), risks
and symptoms to watch for at higher values, verification steps, and
rollback procedure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:31:10 -05:00
brianhansonxyz 4058269762 hyundai canfd: bump steer max 270->324 (~+20%), rate_up 2->3, rate_down 3->5
Tucson (and other Hyundai CAN-FD HDA2 cars) get pinned to comma's
conservative steer ceiling of 270 in the CAN-FD branch of
selfdrive/car/hyundai/values.py and the matching panda safety limit
HYUNDAI_CANFD_STEERING_LIMITS. Comma's non-CAN-FD HKG default is 384;
PR commaai/openpilot#25723 merged 384 for HDA1 EV6/Ioniq 5 with
operator quote "max steer needed to be 384 to make basic turns."

Bumping to 324 (~+20%) keeps a safe margin under the proven 384
ceiling while giving enough headroom for moderate clover-style on-ramps
without the controller hitting the limit. Rate limits also nudged up
(2->3 up, 3->5 down) to match the slightly higher ceiling and reduce
ramp time / release lag; chose 5 instead of comma's 7 for delta_down to
keep release smoother. max_rt_delta scaled proportionally (112->134) so
the 250ms real-time check tracks the new ceiling.

Both files MUST stay in lockstep — panda safety enforces independently
and would reject larger commands if only one side was bumped. Panda
firmware re-flashes automatically on next pandad start because the
safety code hash changed.

References:
  https://github.com/commaai/openpilot/pull/25723   (HDA1 270->384)
  https://github.com/commaai/openpilot/issues/24122 (HKG torque blacklist)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:29:06 -05:00
brianhansonxyz 8b4b7e04b5 adopt modelrevert tree as the new clearpilot baseline
Replaces clearpilot's working state wholesale with the modelrevert branch's
tree (modelrevert tip cea422b). Discards the parked-controlsd manager-process
split and the two session READMEs that documented it; keeps the simpler
in-process park short-circuits (controlsd state_control, plannerd, frogpilot_process)
and the cached-output decimation (modeld, dmonitoringmodeld) that achieve
the same goal with less moving parts. Also brings in the locationd GPS
ignore, the calibrationd valid=calStatus gate, and the model-revert lineage's
controlsd / paramsd / torqued / events.py / carstate.py / interfaces.py.

This is a single new commit on clearpilot (no merge), so the branch advances
linearly while the file state matches modelrevert exactly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:17:35 -05:00
brianhansonxyz d639e28057 unblock startup chain + disable GPS in locationd; document session
Four logically-related changes that together get the manager booting
cleanly end-to-end after the prior session's baseline revert + parked-mode
split, plus a session doc.

boardd: trigger safety_setter_thread on ignition rising edge, not on
IsOnroad rising edge. The parked-mode split broke the stock assumption
that ignition rising implies IsOnroad rising — IsOnroad now requires
`started`, which requires thermald to see carState != park, which
requires controlsd_parked to publish carState, which can't happen until
boardd acks OBD multiplexing. Triggering on ignition edge restores the
"set safety as soon as the bus is alive" intent for both controlsd
variants.

controlsd: drop the two `params_memory.put_bool("no_lat_lane_change",
...)` calls. The key was never registered in common/params.cc so
state_control crashed with UnknownKeyName on first cycle. The carcontroller
reads off `frogpilot_variables.no_lat_lane_change` (in-process), which
controlsd already sets; the UI reads `frogpilotCarControl.NoLatLaneChange`
from cereal, which nobody was setting in the restored controlsd. Add
`self.FPCC.noLatLaneChange = True/False` in the same lane-change branch
so the UI lane-edge indicator reflects state. No actuator change.

cereal/services.py: restore deviceState/managerState declarations to 2Hz
to match the restored DT_TRML=0.5 (thermald at 2Hz). Earlier fan-control
work bumped both to 5Hz; the realtime.py revert undid the thermald rate
bump but services.py wasn't reverted, so freq window [4.0, 6.0]Hz failed
on every cycle and controlsd fired commIssue continuously.

locationd: add a `clearpilot_disable_gps` const at the top of handle_gps
and OR it into the existing reject condition. With it true, every
gpsLocation message falls through to determine_gps_mode() — openpilot's
stock no-GPS path. last_gps_msg never updates, is_gps_ok() permanently
false. gpsd is untouched so UI / dashcamd / clock-set / night-mode auto-
switch keep working unchanged. The user's "drift right on straight roads"
symptom went away after this edit; the previous gpsd.py was hard-coding
vNED=[0,0,0] while the car was moving, feeding the Kalman contradictory
GPS-vs-IMU velocity observations that propagated into latcontrol_torque
through liveLocationKalman.angularVelocityCalibrated. Reversible by
flipping the const to false.

sessions/: a single README documenting this session. Includes the
calibrationd-still-stale investigation — liveCalibration.valid stuck at
False because of a Python SubMaster freq_ok issue with carState under
poll='cameraOdometry' (likely MSGQ NUM_READERS=12 eviction with too many
subscribers). 7ee923b already solved this exact failure mode by gating
calibrationd's publish on calStatus instead of sm.all_checks(); that
commit was reverted in 47321e3 as part of the variable-FPS rollback
but is unrelated to that family and is the natural next move.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:48:11 -05:00
brianhansonxyz 27cad05cd9 sessions: document baseline revert + parked-controlsd architecture
Briefing for the on-device agent covering commits 47321e3, f7e602c,
and 887b9c9: what was reverted vs kept, the UI re-wire, the parked
controlsd swap mechanism, build prerequisites, runtime verification
commands, and a troubleshooting playbook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 09:18:34 -05:00
brianhansonxyz 887b9c9e12 parked-controlsd mode: shut down heavy stack while ignition+park
Adds a second controlsd variant that runs while ignition is on but the
car is in Park. It only listens to CAN and publishes carState — no
model, no planner, no lateral/long control, no actuator commands — so
modeld, locationd, calibrationd, plannerd, radard, paramsd, torqued,
dmonitoring*, soundd, loggerd all stay stopped while parked.

Manager swaps between the two via mutually-exclusive predicates:
  - controlsd_parked: ignition AND not started
  - controlsd (full): started (= ignition AND not_parked)

Thermald owns the swap. It already subscribes to carState; we add a
new onroad condition `not_parked` derived from gearShifter, with a
1.5s hysteresis on going into parked (R/P/D thrash protection) and
zero hysteresis on going out (instant wake on shift to D/R/N). At
boot we assume parked so the heavy stack waits for carState to
confirm gear has actually left Park.

Manager predicates can only see persistent Params, not pandaStates,
so thermald exposes ignition as a new IgnitionOn param (edge-written).
Reverse is treated as not-parked — driver is moving.

Files:
- selfdrive/controls/controlsd_parked.py (new, ~50 lines)
- selfdrive/thermald/thermald.py: not_parked condition + IgnitionOn
- selfdrive/manager/process_config.py: parked_only predicate + entry
- selfdrive/manager/manager.py: seed IgnitionOn=False
- common/params.cc: register IgnitionOn

The full controlsd is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 09:13:41 -05:00
16 changed files with 405 additions and 122 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ from openpilot.system.hardware import PC
# time step for each process
DT_CTRL = 0.01 # controlsd
DT_MDL = 0.05 # model
DT_TRML = 0.5 # thermald and manager
DT_TRML = 0.25 # thermald and manager — 4 Hz
DT_DMON = 0.05 # driver monitoring
+9 -4
View File
@@ -1,11 +1,16 @@
#include "safety_hyundai_common.h"
const SteeringLimits HYUNDAI_CANFD_STEERING_LIMITS = {
.max_steer = 270,
.max_rt_delta = 112,
// CLEARPILOT: bumped from comma defaults (270/2/3/112) by ~20% so this Tucson HDA2
// can hold modest curvature in place. Mirrors the rate-limit shape comma uses for
// its non-CAN-FD high-torque HKG default (384/3/7). Must stay in lockstep with
// STEER_MAX in selfdrive/car/hyundai/values.py — panda enforces independently and
// will reject larger commands if openpilot's value exceeds this.
.max_steer = 324,
.max_rt_delta = 134,
.max_rt_interval = 250000,
.max_rate_up = 2,
.max_rate_down = 3,
.max_rate_up = 3,
.max_rate_down = 5,
.driver_torque_allowance = 250,
.driver_torque_factor = 2,
.type = TorqueDriverLimited,
+27 -57
View File
@@ -13,6 +13,7 @@ from openpilot.selfdrive.car.hyundai.hyundaicanfd import CanBus
from openpilot.selfdrive.car.hyundai.values import HyundaiFlags, CAR, DBC, CAN_GEARS, CAMERA_SCC_CAR, \
CANFD_CAR, Buttons, CarControllerParams
from openpilot.selfdrive.car.interfaces import CarStateBase
from openpilot.selfdrive.clearpilot.telemetry import tlog
PREV_BUTTON_SAMPLES = 8
CLUSTER_SAMPLE_RATE = 20 # frames
@@ -47,6 +48,10 @@ class CarState(CarStateBase):
self.is_metric = False
self.buttons_counter = 0
# CLEARPILOT: cache to avoid per-cycle atomic writes to /dev/shm (eats CPU via fsync/flock)
self._prev_car_speed_limit = None
self._prev_car_is_metric = None
self.cruise_info = {}
# On some cars, CLU15->CF_Clu_VehicleSpeed can oscillate faster than the dash updates. Sample at 5 Hz
@@ -209,10 +214,15 @@ class CarState(CarStateBase):
self.lkas_previously_enabled = self.lkas_enabled
self.lkas_enabled = cp.vl["BCM_PO_11"]["LFA_Pressed"]
# self.params_memory.put_int("CarSpeedLimitLiteral", self.calculate_speed_limit(cp, cp_cam))
self.params_memory.put_float("CarSpeedLimit", self.calculate_speed_limit(cp, cp_cam) * speed_conv)
self.params_memory.put_float("CarCruiseDisplayActual", cp_cruise.vl["SCC11"]["VSetDis"])
# CLEARPILOT: gate on change — see same fix in update_canfd
car_speed_limit = self.calculate_speed_limit(cp, cp_cam) * speed_conv
if car_speed_limit != self._prev_car_speed_limit:
self.params_memory.put_float("CarSpeedLimit", car_speed_limit)
self._prev_car_speed_limit = car_speed_limit
if self.is_metric != self._prev_car_is_metric:
self.params_memory.put("CarIsMetric", "1" if self.is_metric else "0")
self._prev_car_is_metric = self.is_metric
return ret
@@ -415,63 +425,23 @@ class CarState(CarStateBase):
# nonAdaptive = false,
# speedCluster = 0 )
# print("Set limit")
# print(self.calculate_speed_limit(cp, cp_cam))
# self.params_memory.put_float("CarSpeedLimitLiteral", self.calculate_speed_limit(cp, cp_cam))
self.params_memory.put_float("CarSpeedLimit", self.calculate_speed_limit(cp, cp_cam) * speed_factor)
# CLEARPILOT: gate on change — these writes run 100Hz, each is an atomic fsync/flock transaction
car_speed_limit = self.calculate_speed_limit(cp, cp_cam) * speed_factor
if car_speed_limit != self._prev_car_speed_limit:
self.params_memory.put_float("CarSpeedLimit", car_speed_limit)
self._prev_car_speed_limit = car_speed_limit
if self.is_metric != self._prev_car_is_metric:
self.params_memory.put("CarIsMetric", "1" if self.is_metric else "0")
self._prev_car_is_metric = self.is_metric
# CLEARPILOT: CAN-FD telemetry — preserved but disabled. Re-enable by uncommenting (also restore the import).
# from openpilot.selfdrive.clearpilot.telemetry import tlog
#
# CLEARPILOT: telemetry logging — disabled, re-enable when needed
# speed_limit_bus = cp if self.CP.flags & HyundaiFlags.CANFD_HDA2 else cp_cam
# scc = cp_cam.vl["SCC_CONTROL"] if self.CP.flags & HyundaiFlags.CANFD_CAMERA_SCC else cp.vl["SCC_CONTROL"]
# cluster = speed_limit_bus.vl["CLUSTER_SPEED_LIMIT"]
#
# tlog("car", {
# "vEgo": round(ret.vEgo, 3),
# "vEgoRaw": round(ret.vEgoRaw, 3),
# "aEgo": round(ret.aEgo, 3),
# "steeringAngleDeg": round(ret.steeringAngleDeg, 1),
# "gear": str(ret.gearShifter),
# "brakePressed": ret.brakePressed,
# "gasPressed": ret.gasPressed,
# "standstill": ret.standstill,
# "leftBlinker": ret.leftBlinker,
# "rightBlinker": ret.rightBlinker,
# })
#
# tlog("cruise", {
# "enabled": ret.cruiseState.enabled,
# "available": ret.cruiseState.available,
# "speed": round(ret.cruiseState.speed, 3),
# "standstill": ret.cruiseState.standstill,
# "accFaulted": ret.accFaulted,
# "ACCMode": scc.get("ACCMode", 0),
# "VSetDis": scc.get("VSetDis", 0),
# "aReqRaw": round(scc.get("aReqRaw", 0), 3),
# "aReqValue": round(scc.get("aReqValue", 0), 3),
# "DISTANCE_SETTING": scc.get("DISTANCE_SETTING", 0),
# "ACC_ObjDist": round(scc.get("ACC_ObjDist", 0), 1),
# })
#
# tlog("speed_limit", {
# "SPEED_LIMIT_1": cluster.get("SPEED_LIMIT_1", 0),
# "SPEED_LIMIT_2": cluster.get("SPEED_LIMIT_2", 0),
# "SPEED_LIMIT_3": cluster.get("SPEED_LIMIT_3", 0),
# "SCHOOL_ZONE": cluster.get("SCHOOL_ZONE", 0),
# "CHIME_1": cluster.get("CHIME_1", 0),
# "CHIME_2": cluster.get("CHIME_2", 0),
# "SPEED_CHANGE_BLINKING": cluster.get("SPEED_CHANGE_BLINKING", 0),
# "calculated": self.calculate_speed_limit(cp, cp_cam),
# "is_metric": self.is_metric,
# })
#
# tlog("buttons", {
# "cruise_button": self.cruise_buttons[-1],
# "main_button": self.main_buttons[-1],
# "lkas_enabled": self.lkas_enabled,
# "main_enabled": self.main_enabled,
# })
# tlog("car", { ... })
# tlog("cruise", { ... })
# tlog("speed_limit", { ... })
# tlog("buttons", { ... })
return ret
+5 -3
View File
@@ -27,12 +27,14 @@ class CarControllerParams:
self.STEER_STEP = 1 # 100 Hz
if CP.carFingerprint in CANFD_CAR:
self.STEER_MAX = 270
# CLEARPILOT: bumped from comma defaults (270/2/3) by ~20% — must stay in
# lockstep with HYUNDAI_CANFD_STEERING_LIMITS in panda/board/safety/safety_hyundai_canfd.h.
self.STEER_MAX = 324
self.STEER_DRIVER_ALLOWANCE = 250
self.STEER_DRIVER_MULTIPLIER = 2
self.STEER_THRESHOLD = 250
self.STEER_DELTA_UP = 2
self.STEER_DELTA_DOWN = 3
self.STEER_DELTA_UP = 3
self.STEER_DELTA_DOWN = 5
# To determine the limit for your car, find the maximum value that the stock LKAS will request.
# If the max stock LKAS request is <384, add your car to this list.
+9
View File
@@ -484,10 +484,19 @@ class CarInterfaceBase(ABC):
self.silent_steer_warning = True
events.add(EventName.steerTempUnavailableSilent)
else:
# CLEARPILOT: log once per instance of this warning
if not getattr(self, '_steer_fault_logged', False):
import sys
print(f"CLP steerTempUnavailable: steerFaultTemporary={cs_out.steerFaultTemporary} "
f"steeringPressed={cs_out.steeringPressed} standstill={cs_out.standstill} "
f"steering_unpressed={self.steering_unpressed} steeringAngleDeg={cs_out.steeringAngleDeg:.1f} "
f"steeringTorque={cs_out.steeringTorque:.1f} vEgo={cs_out.vEgo:.2f}", file=sys.stderr)
self._steer_fault_logged = True
events.add(EventName.steerTempUnavailable)
else:
self.no_steer_warning = False
self.silent_steer_warning = False
self._steer_fault_logged = False
if cs_out.steerFaultPermanent:
events.add(EventName.steerUnavailable)
+75 -41
View File
@@ -37,10 +37,9 @@ from openpilot.system.version import get_short_branch
from openpilot.selfdrive.frogpilot.controls.lib.frogpilot_functions import CRUISING_SPEED, PROBABILITY, MovingAverageCalculator
from openpilot.selfdrive.frogpilot.controls.lib.model_manager import RADARLESS_MODELS
from openpilot.selfdrive.frogpilot.controls.lib.speed_limit_controller import SpeedLimitController
# CLEARPILOT: UI plumbing for ScreenDisplayMode and the speed/cruise-warning overlay
from openpilot.selfdrive.clearpilot.telemetry import tlog
from openpilot.selfdrive.clearpilot.speed_logic import SpeedState
from openpilot.selfdrive.frogpilot.controls.lib.speed_limit_controller import SpeedLimitController
SOFT_DISABLE_TIME = 3 # seconds
LDW_MIN_SPEED = 31 * CV.MPH_TO_MS
@@ -50,7 +49,7 @@ CAMERA_OFFSET = 0.04
REPLAY = "REPLAY" in os.environ
SIMULATION = "SIMULATION" in os.environ
TESTING_CLOSET = "TESTING_CLOSET" in os.environ
IGNORE_PROCESSES = {"loggerd", "encoderd", "statsd"}
IGNORE_PROCESSES = {"loggerd", "encoderd", "statsd", "telemetryd", "dashcamd"}
ThermalStatus = log.DeviceState.ThermalStatus
State = log.ControlsState.OpenpilotState
@@ -80,14 +79,13 @@ class Controls:
self.params_storage = Params("/persist/params")
self.params_memory.put_bool("CPTLkasButtonAction", False)
# CLEARPILOT: ScreenDisplayMode is an int (5-state machine: 0..4); UI reads it via getInt
self.params_memory.put_int("ScreenDisplayMode", 0)
# CLEARPILOT: speed/cruise-warning overlay state, ticked at ~2Hz from clearpilot_state_control
# CLEARPILOT: speed-limit/cruise-warning state machine + park→drive auto-reset
self.speed_state = SpeedState()
self.speed_state_frame = 0
# CLEARPILOT: edge tracking for park->drive auto-wake of screen
self.was_driving_gear = False
self.driving_gear = False
self.radarless_model = self.params.get("Model", encoding='utf-8') in RADARLESS_MODELS
@@ -451,6 +449,13 @@ class Controls:
# All events here should at least have NO_ENTRY and SOFT_DISABLE.
num_events = len(self.events)
# CLEARPILOT: compute model standby suppression early — used by multiple checks below
try:
standby_ts = float(self.params_memory.get("ModelStandbyTs") or "0")
except (ValueError, TypeError):
standby_ts = 0
model_suppress = (time.monotonic() - standby_ts) < 2.0
not_running = {p.name for p in self.sm['managerState'].processes if not p.running and p.shouldBeRunning}
if self.sm.recv_frame['managerState'] and (not_running - IGNORE_PROCESSES):
self.events.add(EventName.processNotRunning)
@@ -464,9 +469,11 @@ class Controls:
elif not self.sm.all_freq_ok(self.camera_packets):
self.events.add(EventName.cameraFrameRate)
if not REPLAY and self.rk.lagging:
import sys
print(f"CLP controlsdLagging: remaining={self.rk.remaining:.4f} standstill={CS.standstill} vEgo={CS.vEgo:.2f}", file=sys.stderr)
self.events.add(EventName.controlsdLagging)
if not self.radarless_model:
if len(self.sm['radarState'].radarErrors) or (not self.rk.lagging and not self.sm.all_checks(['radarState'])):
if not model_suppress and (len(self.sm['radarState'].radarErrors) or (not self.rk.lagging and not self.sm.all_checks(['radarState']))):
self.events.add(EventName.radarFault)
if not self.sm.valid['pandaStates']:
self.events.add(EventName.usbError)
@@ -478,13 +485,16 @@ class Controls:
# generic catch-all. ideally, a more specific event should be added above instead
has_disable_events = self.events.contains(ET.NO_ENTRY) and (self.events.contains(ET.SOFT_DISABLE) or self.events.contains(ET.IMMEDIATE_DISABLE))
no_system_errors = (not has_disable_events) or (len(self.events) == num_events)
if (not self.sm.all_checks() or self.card.can_rcv_timeout) and no_system_errors:
# CLEARPILOT: fire commIssue ONLY when messages actually aren't flowing (not_alive)
# or CAN RX is timing out. Don't fire on self-declared valid=False — that's the
# polling-pattern / all_checks cascade that paramsd/torqued/plannerd/frogpilot
# propagate even while their publish rate and content are fine.
comms_really_broken = (not self.sm.all_alive()) or self.card.can_rcv_timeout
if comms_really_broken and no_system_errors and not model_suppress:
if not self.sm.all_alive():
self.events.add(EventName.commIssue)
elif not self.sm.all_freq_ok():
self.events.add(EventName.commIssueAvgFreq)
else: # invalid or can_rcv_timeout.
self.events.add(EventName.commIssue)
else:
self.events.add(EventName.commIssue) # can_rcv_timeout path
logs = {
'invalid': [s for s, valid in self.sm.valid.items() if not valid],
@@ -499,13 +509,13 @@ class Controls:
self.logged_comm_issue = None
if not (self.CP.notCar and self.joystick_mode):
if not self.sm['liveLocationKalman'].posenetOK:
if not self.sm['liveLocationKalman'].posenetOK and not model_suppress:
self.events.add(EventName.posenetInvalid)
if not self.sm['liveLocationKalman'].deviceStable:
self.events.add(EventName.deviceFalling)
if not self.sm['liveLocationKalman'].inputsOK:
if not self.sm['liveLocationKalman'].inputsOK and not model_suppress:
self.events.add(EventName.locationdTemporaryError)
if not self.sm['liveParameters'].valid and not TESTING_CLOSET and (not SIMULATION or REPLAY):
if not self.sm['liveParameters'].valid and not TESTING_CLOSET and (not SIMULATION or REPLAY) and not model_suppress:
self.events.add(EventName.paramsdTemporaryError)
# conservative HW alert. if the data or frequency are off, locationd will throw an error
@@ -551,7 +561,7 @@ class Controls:
self.distance_traveled = 0
self.distance_traveled += CS.vEgo * DT_CTRL
if self.sm['modelV2'].frameDropPerc > 20:
if self.sm['modelV2'].frameDropPerc > 20 and not model_suppress:
self.events.add(EventName.modeldLagging)
@@ -638,6 +648,14 @@ class Controls:
# Check if openpilot is engaged and actuators are enabled
self.enabled = self.state in ENABLED_STATES
self.active = self.state in ACTIVE_STATES
# CLEARPILOT: engagement telemetry disabled — was running at 100Hz, causing CPU load
# tlog("engage", {
# "state": self.state.name if hasattr(self.state, 'name') else str(self.state),
# "enabled": self.enabled, "active": self.active,
# "cruise_enabled": CS.cruiseState.enabled, "cruise_available": CS.cruiseState.available,
# "brakePressed": CS.brakePressed,
# })
if self.active:
self.current_alert_types.append(ET.WARNING)
@@ -647,6 +665,30 @@ class Controls:
def state_control(self, CS):
"""Given the state, this function returns a CarControl packet"""
# CLEARPILOT: short-circuit while parked. Skip LaC/LoC PID, MPC, model_v2
# reads, lane-change logic — none of it matters when the car isn't moving.
# publish_logs still runs and still triggers carcontroller.apply via
# card.controls_update, so the sendcan heartbeats / tester-present messages
# keep flowing at 100Hz and the car doesn't fault. Saves ~30% controlsd CPU
# in park.
if CS.gearShifter == car.CarState.GearShifter.park:
CC = car.CarControl.new_message()
CC.enabled = False
CC.latActive = False
CC.longActive = False
CC.actuators.longControlState = self.LoC.long_control_state
self.LaC.reset()
self.LoC.reset(v_pid=CS.vEgo)
self.frogpilot_variables.no_lat_lane_change = False
self.FPCC.noLatLaneChange = False
# Call LaC.update with active=False so we get the right lac_log subtype
# for this car's lateralTuning (torque vs pid vs angle). Internally it
# early-returns when active is False — cheap.
lp = self.sm['liveParameters']
_, _, lac_log = self.LaC.update(False, CS, self.VM, lp, self.steer_limited, 0.0,
self.sm['liveLocationKalman'], model_data=self.sm['modelV2'])
return CC, lac_log
# Update VehicleModel
lp = self.sm['liveParameters']
x = max(lp.stiffnessFactor, 0.1)
@@ -686,12 +728,11 @@ class Controls:
if model_v2.meta.laneChangeState == LaneChangeState.laneChangeStarting and clearpilot_disable_lat_on_lane_change:
CC.latActive = False
self.params_memory.put_bool("no_lat_lane_change", True)
# CLEARPILOT: hyundai carcontroller reads this off frogpilot_variables (not Params) — keep both in sync
self.frogpilot_variables.no_lat_lane_change = True
self.FPCC.noLatLaneChange = True
else:
self.params_memory.put_bool("no_lat_lane_change", False)
self.frogpilot_variables.no_lat_lane_change = False
self.FPCC.noLatLaneChange = False
if CS.leftBlinker or CS.rightBlinker:
self.last_blinker_frame = self.sm.frame
@@ -1246,47 +1287,40 @@ class Controls:
self.frogpilot_variables.use_ev_tables = self.params.get_bool("EVTable")
def update_clearpilot_events(self, CS):
if (len(CS.buttonEvents) > 0):
if (len(CS.buttonEvents) > 0):
print (CS.buttonEvents)
# Uncomment to alert when lkas button pressed
# if any(be.pressed and be.type == FrogPilotButtonType.lkas for be in CS.buttonEvents):
# self.events.add(EventName.clpDebug)
if any(be.pressed and be.type == FrogPilotButtonType.lkas for be in CS.buttonEvents):
self.events.add(EventName.clpDebug)
def clearpilot_state_control(self, CC, CS):
# CLEARPILOT: pure UI plumbing — does not modify CC/actuators. Maintains
# ScreenDisplayMode (5-state machine driven by the LFA/debug button + gear
# edges) and ticks the speed/cruise-warning overlay at ~2Hz.
driving_gear = CS.gearShifter not in (GearShifter.neutral, GearShifter.park,
GearShifter.reverse, GearShifter.unknown)
# Auto-wake screen when shifting into drive from screen-off
if driving_gear and not self.was_driving_gear:
# CLEARPILOT: auto-reset display when shifting into drive from screen-off
if self.driving_gear and not self.was_driving_gear:
if self.params_memory.get_int("ScreenDisplayMode") == 3:
self.params_memory.put_int("ScreenDisplayMode", 0)
self.was_driving_gear = driving_gear
self.was_driving_gear = self.driving_gear
# LFA/debug button cycles ScreenDisplayMode. Onroad and offroad use
# different transition tables.
if any(be.pressed and be.type == FrogPilotButtonType.lkas for be in CS.buttonEvents):
current = self.params_memory.get_int("ScreenDisplayMode")
if driving_gear:
if self.driving_gear:
# Onroad: 0→4, 1→2, 2→3, 3→4, 4→2 (never back to auto via button)
transitions = {0: 4, 1: 2, 2: 3, 3: 4, 4: 2}
new_mode = transitions.get(current, 0)
else:
# Not in drive: anything except 3 → 3 (screen off), state 3 → 0 (auto)
# Not in drive: any except 3 → 3, state 3 → 0
new_mode = 0 if current == 3 else 3
self.params_memory.put_int("ScreenDisplayMode", new_mode)
# Speed/cruise-warning overlay tick (~2Hz at 100Hz loop)
# ClearPilot speed processing (~2 Hz at 100 Hz loop) — drives speed-limit sign
# and cruise over/under warning sign via memory params read by the UI.
self.speed_state_frame += 1
if self.speed_state_frame % 50 == 0:
gps = self.sm['gpsLocation']
has_gps = self.sm.valid['gpsLocation'] and gps.hasFix
speed_ms = gps.speed if has_gps else 0.0
speed_limit_ms = self.params_memory.get_float("CarSpeedLimit") or 0.0
is_metric = self.is_metric
speed_limit_ms = self.params_memory.get_float("CarSpeedLimit")
is_metric = (self.params_memory.get("CarIsMetric", encoding="utf-8") or "0") == "1"
cruise_speed_ms = CS.cruiseState.speed
cruise_active = CS.cruiseState.enabled
cruise_standstill = CS.cruiseState.standstill
+1 -1
View File
@@ -778,8 +778,8 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
ET.SOFT_DISABLE: soft_disable_alert("Sensor Data Invalid"),
},
# CLEARPILOT: alert suppressed — event still fires for screen toggle and future actions
EventName.clpDebug: {
ET.PERMANENT: clp_debug_notice,
},
EventName.noGps: {
+4
View File
@@ -34,6 +34,10 @@ def plannerd_thread():
while True:
sm.update()
if sm.updated['modelV2']:
# CLEARPILOT: skip planning while parked. The downstream consumer (controlsd)
# already short-circuits in park, so longitudinalPlan/uiPlan staleness is fine.
if sm['carState'].gearShifter == car.CarState.GearShifter.park:
continue
longitudinal_planner.update(sm)
longitudinal_planner.publish(sm, pm)
publish_ui_plan(sm, pm, longitudinal_planner)
@@ -58,6 +58,9 @@ class FrogPilotPlanner:
self.params = Params()
self.params_memory = Params("/dev/shm/params")
# CLEARPILOT: track valid transitions so we only log when it flips, not every cycle
self._dbg_prev_valid = True
self.cem = ConditionalExperimentalMode()
self.lead_one = Lead()
self.mtsc = MapTurnSpeedController()
@@ -242,7 +245,18 @@ class FrogPilotPlanner:
def publish(self, sm, pm):
frogpilot_plan_send = messaging.new_message('frogpilotPlan')
frogpilot_plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState'])
valid = sm.all_checks(service_list=['carState', 'controlsState'])
# CLEARPILOT: log on transition into invalid — stderr goes to our plannerd.log
if valid != self._dbg_prev_valid and not valid:
import sys
print(
"CLP frogpilotPlan valid=False: "
f"carState(a={sm.alive['carState']},v={sm.valid['carState']},f={sm.freq_ok['carState']}) "
f"controlsState(a={sm.alive['controlsState']},v={sm.valid['controlsState']},f={sm.freq_ok['controlsState']})",
file=sys.stderr, flush=True
)
self._dbg_prev_valid = valid
frogpilot_plan_send.valid = valid
frogpilotPlan = frogpilot_plan_send.frogpilotPlan
frogpilotPlan.accelerationJerk = A_CHANGE_COST * (float(self.jerk) if self.lead_one.status else 1)
+3 -1
View File
@@ -85,7 +85,9 @@ def frogpilot_thread():
frogpilot_planner = FrogPilotPlanner(CP)
frogpilot_planner.update_frogpilot_params()
if sm.updated['modelV2']:
# CLEARPILOT: skip planner work while parked.
parked = sm['carState'].gearShifter == car.CarState.GearShifter.park
if sm.updated['modelV2'] and not parked:
frogpilot_planner.update(sm['carState'], sm['controlsState'], sm['frogpilotCarControl'], sm['frogpilotNavigation'],
sm['liveLocationKalman'], sm['modelV2'], sm['radarState'])
frogpilot_planner.publish(sm, pm)
+8 -1
View File
@@ -284,7 +284,14 @@ def main() -> NoReturn:
# 4Hz driven by cameraOdometry
if sm.frame % 5 == 0:
calibrator.send_data(pm, sm.all_checks())
# CLEARPILOT: publish valid based on calibration status, not upstream sm.all_checks().
# The original gate cascaded upstream freq glitches into liveCalibration.valid=False,
# which kept locationd.filterInitialized False, which fed garbage into paramsd, which
# corrupted steerRatio and caused erratic steering (and controlsd commIssue banners).
# "valid" here semantically means "the calibration data is trustworthy" — a question
# about convergence, not input freshness.
cal_valid = calibrator.cal_status == log.LiveCalibrationData.Status.calibrated
calibrator.send_data(pm, cal_valid)
if __name__ == "__main__":
+7 -1
View File
@@ -308,12 +308,18 @@ void Localizer::input_fake_gps_observations(double current_time) {
}
void Localizer::handle_gps(double current_time, const cereal::GpsLocationData::Reader& log, const double sensor_time_offset) {
// CLEARPILOT: GPS disabled as a Kalman input. gpsd still publishes real GPS for
// UI / dashcam / clock / night-mode; only locationd ignores it. Falls through to
// determine_gps_mode() which is openpilot's existing no-GPS path (fake observations
// to bound position uncertainty). To re-enable, set this to false.
const bool clearpilot_disable_gps = true;
bool gps_unreasonable = (Vector2d(log.getHorizontalAccuracy(), log.getVerticalAccuracy()).norm() >= SANE_GPS_UNCERTAINTY);
bool gps_accuracy_insane = ((log.getVerticalAccuracy() <= 0) || (log.getSpeedAccuracy() <= 0) || (log.getBearingAccuracyDeg() <= 0));
bool gps_lat_lng_alt_insane = ((std::abs(log.getLatitude()) > 90) || (std::abs(log.getLongitude()) > 180) || (std::abs(log.getAltitude()) > ALTITUDE_SANITY_CHECK));
bool gps_vel_insane = (floatlist2vector(log.getVNED()).norm() > TRANS_SANITY_CHECK);
if (!log.getHasFix() || gps_unreasonable || gps_accuracy_insane || gps_lat_lng_alt_insane || gps_vel_insane) {
if (clearpilot_disable_gps || !log.getHasFix() || gps_unreasonable || gps_accuracy_insane || gps_lat_lng_alt_insane || gps_vel_insane) {
//this->gps_valid = false;
this->determine_gps_mode(current_time);
return;
+13 -3
View File
@@ -7,7 +7,7 @@ import ctypes
import numpy as np
from pathlib import Path
from cereal import messaging
from cereal import car, messaging
from cereal.messaging import PubMaster, SubMaster
from cereal.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
from openpilot.common.swaglog import cloudlog
@@ -128,10 +128,15 @@ def main():
assert vipc_client.is_connected()
cloudlog.warning(f"connected with buffer size: {vipc_client.buffer_len}")
sm = SubMaster(["liveCalibration"])
sm = SubMaster(["liveCalibration", "carState"])
pm = PubMaster(["driverStateV2"])
calib = np.zeros(CALIB_LEN, dtype=np.float32)
# CLEARPILOT: cache last model output to serve while gear is in park —
# mirrors the same trick modeld uses. Skips DSP inference on the driver
# camera when the car is stationary; downstream dmonitoringd still gets
# a fresh publish each frame.
last_model_output = None
# last = 0
while True:
@@ -143,8 +148,13 @@ def main():
if sm.updated["liveCalibration"]:
calib[:] = np.array(sm["liveCalibration"].rpyCalib)
parked = sm["carState"].gearShifter == car.CarState.GearShifter.park
t1 = time.perf_counter()
model_output, dsp_execution_time = model.run(buf, calib)
if parked and last_model_output is not None:
model_output, dsp_execution_time = last_model_output
else:
model_output, dsp_execution_time = model.run(buf, calib)
last_model_output = (model_output, dsp_execution_time)
t2 = time.perf_counter()
pm.send("driverStateV2", get_driverstate_packet(model_output, vipc_client.frame_id, vipc_client.timestamp_sof, t2 - t1, dsp_execution_time))
+29 -6
View File
@@ -134,11 +134,15 @@ def main(demo=False):
setproctitle(PROCESS_NAME)
config_realtime_process(7, 54)
import time as _time
cloudlog.warning("setting up CL context")
_t0 = _time.monotonic()
cl_context = CLContext()
cloudlog.warning("CL context ready; loading model")
_t1 = _time.monotonic()
cloudlog.warning("CL context ready in %.3fs; loading model", _t1 - _t0)
model = ModelState(cl_context)
cloudlog.warning("models loaded, modeld starting")
_t2 = _time.monotonic()
cloudlog.warning("model loaded in %.3fs (total init %.3fs), modeld starting", _t2 - _t1, _t2 - _t0)
# visionipc clients
while True:
@@ -179,6 +183,10 @@ def main(demo=False):
model_transform_main = np.zeros((3, 3), dtype=np.float32)
model_transform_extra = np.zeros((3, 3), dtype=np.float32)
live_calib_seen = False
# CLEARPILOT: cache last model output to serve while gear is in park — saves
# GPU inference cost while still giving downstream a constant publish rate so
# freq_ok / valid checks don't cascade.
last_model_output = None
nav_features = np.zeros(ModelConstants.NAV_FEATURE_LEN, dtype=np.float32)
nav_instructions = np.zeros(ModelConstants.NAV_INSTRUCTION_LEN, dtype=np.float32)
buf_main, buf_extra = None, None
@@ -233,6 +241,12 @@ def main(demo=False):
meta_extra = meta_main
sm.update(0)
# CLEARPILOT: constant 20fps. Variable-rate + standby logic removed — the
# variable-rate path caused freq_ok cascades in downstream consumers
# (calibrationd/locationd/paramsd). Running at the camera's native rate is
# simpler and keeps the full-stack localization chain happy.
desire = DH.desire
is_rhd = sm["driverMonitoringState"].isRHD
frame_id = sm["roadCameraState"].frameId
@@ -304,12 +318,21 @@ def main(demo=False):
**({'radar_tracks': radar_tracks,} if DISABLE_RADAR else {}),
}
mt1 = time.perf_counter()
model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only)
mt2 = time.perf_counter()
model_execution_time = mt2 - mt1
# CLEARPILOT: in park, serve the cached last model output instead of running
# GPU inference. First cycle (no cache yet) still runs once so we have
# something to serve. Out-of-park resumes fresh inference every frame.
parked = sm['carState'].gearShifter == car.CarState.GearShifter.park
if parked and last_model_output is not None:
model_output = last_model_output
model_execution_time = 0.0
else:
mt1 = time.perf_counter()
model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only)
mt2 = time.perf_counter()
model_execution_time = mt2 - mt1
if model_output is not None:
last_model_output = model_output
modelv2_send = messaging.new_message('modelV2')
posenet_send = messaging.new_message('cameraOdometry')
fill_model_msg(modelv2_send, model_output, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, frame_drop_ratio,
+22 -2
View File
@@ -21,6 +21,7 @@ def dmonitoringd_thread():
v_cruise_last = 0
driver_engaged = False
dbg_prev_valid = True # CLEARPILOT: track valid transitions
# 10Hz <- dmonitoringmodeld
while True:
@@ -43,7 +44,18 @@ def dmonitoringd_thread():
# Get data from dmonitoringmodeld
events = Events()
if sm.all_checks() and len(sm['liveCalibration'].rpyCalib):
# CLEARPILOT: narrow update_states gate. The original sm.all_checks() also
# required modelV2 fresh (stops at standstill in two-state modeld) and
# liveCalibration.valid (calibrationd cascades its own freq_ok to valid, which
# flaps). Both made DM freeze pose → face_detected stuck False → awareness
# decayed to 0 within 6s of engagement. Narrow the gate to the subs
# update_states actually reads, and only to alive+valid (skip freq_ok and
# skip liveCalibration.valid). rpyCalib presence is sufficient to know
# calibration has produced output.
if (sm.alive['driverStateV2'] and sm.valid['driverStateV2'] and
sm.alive['carState'] and sm.valid['carState'] and
sm.alive['controlsState'] and sm.valid['controlsState'] and
sm.alive['liveCalibration'] and len(sm['liveCalibration'].rpyCalib) > 0):
driver_status.update_states(sm['driverStateV2'], sm['liveCalibration'].rpyCalib, sm['carState'].vEgo, sm['controlsState'].enabled)
# Block engaging after max number of distrations
@@ -54,8 +66,16 @@ def dmonitoringd_thread():
# Update events from driver state
driver_status.update_events(events, driver_engaged, sm['controlsState'].enabled, sm['carState'].standstill)
# CLEARPILOT: log on transition to invalid so we can see which sub caused the cascade
dm_valid = sm.all_checks()
if dm_valid != dbg_prev_valid and not dm_valid:
import sys
bad = [s for s in sm.alive if not (sm.alive[s] and sm.valid[s] and sm.freq_ok.get(s, True))]
details = [f"{s}(a={sm.alive[s]},v={sm.valid[s]},f={sm.freq_ok[s]})" for s in bad]
print(f"CLP driverMonitoringState valid=False: {' '.join(details)}", file=sys.stderr, flush=True)
dbg_prev_valid = dm_valid
# build driverMonitoringState packet
dat = messaging.new_message('driverMonitoringState', valid=sm.all_checks())
dat = messaging.new_message('driverMonitoringState', valid=dm_valid)
dat.driverMonitoringState = {
"events": events.to_msg(),
"faceDetected": driver_status.face_detected,
@@ -0,0 +1,177 @@
# Session: 2026-04-26 — Hyundai CAN-FD steering torque bump
Documentation for commit `4058269` on `clearpilot`. Reference for any
future re-tuning.
## What changed
Bumped this Tucson HDA2 (CAN-FD platform) from comma's conservative
steer ceiling to a value that gives more headroom on tighter on-ramp
clovers, plus a small rate-limit nudge so the controller can actually
reach the new ceiling within reasonable transient time.
| Constant | Before (comma default for CAN-FD) | After (this commit) | Comma's non-CAN-FD HKG default |
|---|---:|---:|---:|
| `max_steer` / `STEER_MAX` | 270 | **324** | 384 |
| `max_rate_up` / `STEER_DELTA_UP` | 2 | **3** | 3 |
| `max_rate_down` / `STEER_DELTA_DOWN` | 3 | **5** | 7 |
| `max_rt_delta` | 112 | **134** | ~150 |
## Where the change lives — TWO files in lockstep
The panda safety firmware enforces these limits **independently** of
openpilot. If only one side is bumped, panda rejects the larger
commands and you get cut-out / `commIssue` behavior. **Always change
both, always to the same numbers, in the same commit.**
1. **`panda/board/safety/safety_hyundai_canfd.h`** (lines 3-19)
- `HYUNDAI_CANFD_STEERING_LIMITS` struct: `max_steer`, `max_rate_up`,
`max_rate_down`, `max_rt_delta`.
- This is C; modifying it changes the panda firmware hash, which
forces an automatic re-flash on next `pandad` start. No manual
panda flash command needed.
2. **`selfdrive/car/hyundai/values.py`** (CAN-FD branch of
`CarControllerParams.__init__`, lines 29-36)
- `STEER_MAX`, `STEER_DELTA_UP`, `STEER_DELTA_DOWN`.
- Pure Python; picked up on next `controlsd` start.
## What each constant does
- **`max_steer` / `STEER_MAX`** — peak torque magnitude the controller
can request. Hard ceiling. Going past this is the headline "more
torque" knob.
- **`max_rate_up` / `STEER_DELTA_UP`** — per-100Hz-cycle upward slew
cap. Higher = faster ramp into a turn. With `max_rate_up = 3` and
`max_steer = 324`, time from 0 to ceiling is 324 / 3 = 108 cycles =
1.08 s.
- **`max_rate_down` / `STEER_DELTA_DOWN`** — per-cycle downward slew
cap. Higher = faster release back toward straight. We chose 5 (vs
comma's 7) for a smoother release feel.
- **`max_rt_delta`** — cumulative torque change allowed across a
rolling 250 ms window (`max_rt_interval`). It's a long-window
envelope check, separate from the per-cycle rate. Should scale with
`max_steer` — we used `max_steer × ~0.41` to mirror comma's ratio.
- **`driver_torque_allowance` / `STEER_DRIVER_ALLOWANCE`** — driver
wheel torque (Nm read off the wheel) that's tolerated before the
system starts derating its own command. Left at 250.
- **`driver_torque_factor` / `STEER_DRIVER_MULTIPLIER`** — how
aggressively the system fights driver input above the allowance.
Left at 2.
## How to go higher (path to 384)
`384` is the community-consensus safe ceiling for HKG. Comma uses it
as the default for every non-CAN-FD HKG that isn't on the explicit
255-blacklist, and they merged it for HDA1 CAN-FD (EV6 / Ioniq 5) in
[openpilot PR #25723](https://github.com/commaai/openpilot/pull/25723).
The PR author noted "max steer needed to be 384 to make basic turns."
Tucson NX4 HDA2 is **not** on the 255-blacklist (see
[issue #24122](https://github.com/commaai/openpilot/issues/24122) for
the verified blacklist).
To go from 324 → 384:
```c
// panda/board/safety/safety_hyundai_canfd.h
.max_steer = 384,
.max_rt_delta = 158, // ~max_steer × 0.41
.max_rate_up = 3,
.max_rate_down = 5, // or 7 for comma-matched aggressive release
```
```python
# selfdrive/car/hyundai/values.py CAN-FD branch
self.STEER_MAX = 384
self.STEER_DELTA_UP = 3
self.STEER_DELTA_DOWN = 5 # or 7
```
Beyond 384 is uncharted for HKG — comma has not tested past it. Some
forks (sunnypilot has discussions) try higher for very heavy vehicles
but with mixed results. Don't go past 384 without an explicit reason.
## Things to watch for / community-flagged risks at higher values
1. **EPS time-out cut every ~90 frames.** The CAN-FD safety already
forces a brief torque cut to stop the EPS from faulting (the
`min_valid_request_frames = 89`, `max_invalid_request_frames = 2`,
`min_valid_request_rt_interval = 810000` block of
`HYUNDAI_CANFD_STEERING_LIMITS`). This is independent of
`max_steer`. Bumping the ceiling does not lengthen the cut-free
window — you just hold the higher torque for the same ~890 ms before
the brief cut. If you're getting `Steering Temporarily Unavailable`
*during sustained turns*, the issue is this cut, not the ceiling,
and it can't be tuned without risking a real EPS fault.
2. **`steerTempUnavailable` / "Cruise Fault: Restart the Car".** Has
been reported on cars in the 255-blacklist when pushed to 384.
Tucson NX4 is not blacklisted, so 324384 is normally safe — but if
you see this alert during slow sweeping turns, that's the symptom.
Roll back to 270 and confirm.
3. **Lateral accel retune.** Higher torque headroom can cause the
torque controller to overshoot if `latAccelFactor` was tuned for
the old ceiling. EV6/Ioniq 5 testing in PR #25723 had to drop
lateral accel to 2.5 m/s² when bumping from 270 to 384. Watch for
over-correction (zig-zag in lane center) after a bump and retune
`latAccelFactor` in `selfdrive/locationd/torqued.py` or via the
torque-tune Params if needed.
4. **Driver-fight feel.** `driver_torque_allowance = 250` /
`driver_torque_factor = 2` is the blending knob. Most "openpilot
fights my hands" complaints come from people who lowered allowance
or raised factor. Don't touch these without a specific reason.
5. **Panda safety hash mismatch.** Changing
`safety_hyundai_canfd.h` regenerates the panda firmware binary with
a different signed hash. On the next `pandad` start, pandad detects
the mismatch and re-flashes the panda automatically (see
`pandad.log`: "Panda firmware out of date" → "flash: flashing" →
"Done flashing"). Takes ~10 s. No manual action needed; just expect
a brief delay before controls come up.
## Verifying the change took effect
```bash
# 1. Confirm the rebuild happened and panda firmware was re-signed.
grep "panda/board/obj/panda_h7" /tmp/build_only.log 2>/dev/null # or run build_only.sh and watch for "signing N bytes"
# 2. Watch for re-flash on launch.
tail -f /data/log2/current/pandad.log
# Should see: "Panda firmware out of date, update required"
# "flash: flashing"
# "Done flashing"
# 3. Confirm the openpilot side is using the new value.
su - comma -c 'PYTHONPATH=/data/openpilot python3 -c "
from cereal import car
from openpilot.common.params import Params
cp_bytes = Params().get(\"CarParams\")
with car.CarParams.from_bytes(cp_bytes) as cp:
print(\"safetyConfig safetyParam:\", [c.safetyParam for c in cp.safetyConfigs])
"'
# 4. Live-check the actual commanded torque ceiling (drive a moderate turn).
# carControl.actuators.steer should now be able to peak above 0.79 (270/255 normalized)
# but stay under 1.0 (full saturation at the new ceiling).
```
## Rollback
If anything misbehaves, revert just the two-file commit:
```bash
git revert 4058269
chown -R comma:comma /data/openpilot
su - comma -c "bash /data/openpilot/build_only.sh"
# Next pandad launch will re-flash back to 270.
```
## Sources
- [openpilot PR #25723 — HDA1 EV6/Ioniq 5 270 → 384](https://github.com/commaai/openpilot/pull/25723)
- [openpilot issue #24122 — HKG torque blacklist verification](https://github.com/commaai/openpilot/issues/24122)
- [openpilot PR #26427 — Hyundai Tucson 2023 support](https://github.com/commaai/openpilot/pull/26427)
- [sunnypilot — increasing torque help](https://community.sunnypilot.ai/t/increasing-torque-help-needed/2082)
- [sunnypilot — raising torque for heavier vehicles](https://community.sunnypilot.ai/t/raising-the-torque-for-heavier-vehicles/862)