Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37e095eab7 | |||
| 5d76576a15 | |||
| 4058269762 | |||
| 8b4b7e04b5 | |||
| d639e28057 | |||
| 27cad05cd9 | |||
| 887b9c9e12 | |||
| f7e602c00b | |||
| 47321e3867 |
@@ -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,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.
|
||||
|
||||
+95
-180
@@ -81,9 +81,11 @@ class Controls:
|
||||
self.params_memory.put_bool("CPTLkasButtonAction", False)
|
||||
self.params_memory.put_int("ScreenDisplayMode", 0)
|
||||
|
||||
# ClearPilot speed processing
|
||||
# CLEARPILOT: speed-limit/cruise-warning state machine + park→drive auto-reset
|
||||
self.speed_state = SpeedState()
|
||||
self.speed_state_frame = 0
|
||||
self.was_driving_gear = False
|
||||
self.driving_gear = False
|
||||
|
||||
self.radarless_model = self.params.get("Model", encoding='utf-8') in RADARLESS_MODELS
|
||||
|
||||
@@ -114,8 +116,7 @@ class Controls:
|
||||
'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'liveLocationKalman',
|
||||
'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters',
|
||||
'testJoystick', 'frogpilotPlan', 'gpsLocation'] + self.camera_packets + self.sensor_packets,
|
||||
ignore_alive=ignore+['gpsLocation'], ignore_avg_freq=ignore+['radarState', 'testJoystick', 'gpsLocation'],
|
||||
ignore_valid=['testJoystick', 'gpsLocation'],
|
||||
ignore_alive=ignore + ['gpsLocation'], ignore_avg_freq=ignore+['radarState', 'testJoystick', 'gpsLocation'], ignore_valid=['testJoystick', 'gpsLocation'],
|
||||
frequency=int(1/DT_CTRL))
|
||||
|
||||
self.joystick_mode = self.params.get_bool("JoystickDebugMode")
|
||||
@@ -170,26 +171,6 @@ class Controls:
|
||||
self.current_alert_types = [ET.PERMANENT]
|
||||
self.logged_comm_issue = None
|
||||
self.not_running_prev = None
|
||||
self.lat_requested_ts = 0 # CLEARPILOT: timestamp when lat was first requested (ramp-up delay + comm issue suppression)
|
||||
self.prev_lat_requested = False
|
||||
# CLEARPILOT: gate frogpilotCarControl send on change (3 static bools, was publishing 100Hz)
|
||||
self._prev_fpcc = (None, None, None, None, None)
|
||||
self._last_fpcc_send_frame = 0
|
||||
# CLEARPILOT: hysteresis counters for transient-filter on cascade alerts. A blip
|
||||
# under 50ms (5 cycles at 100Hz) is typical of MSGQ conflate + slow-polling-consumer
|
||||
# freq-measurement artifacts — not a real comm issue. Persistent failure still
|
||||
# alerts after 50ms, well inside a driver's reaction window.
|
||||
self._hyst_commissue = 0
|
||||
self._hyst_locationd_tmp = 0
|
||||
self._hyst_paramsd_tmp = 0
|
||||
self._hyst_posenet = 0
|
||||
self.HYST_CYCLES = 5
|
||||
# CLEARPILOT: parked-cycle skip — in park+ignition-on, run the full step() only
|
||||
# every 10th cycle. CAN parse + CAN TX still happen every cycle; outer logic
|
||||
# (events, state machine, PID/MPC, publishing) runs at 10Hz. Cached message
|
||||
# bytes are re-published on skipped cycles so downstream freq_ok stays OK.
|
||||
self._cached_controlsState_bytes = None
|
||||
self._cached_carControl_bytes = None
|
||||
self.steer_limited = False
|
||||
self.desired_curvature = 0.0
|
||||
self.experimental_mode = False
|
||||
@@ -224,8 +205,6 @@ class Controls:
|
||||
self.always_on_lateral_main = self.always_on_lateral and self.params.get_bool("AlwaysOnLateralMain")
|
||||
|
||||
self.drive_added = False
|
||||
self.driving_gear = False
|
||||
self.was_driving_gear = False
|
||||
self.fcw_random_event_triggered = False
|
||||
self.holiday_theme_alerted = False
|
||||
self.onroad_distance_pressed = False
|
||||
@@ -264,51 +243,27 @@ class Controls:
|
||||
CS = self.data_sample()
|
||||
cloudlog.timestamp("Data sampled")
|
||||
|
||||
# CLEARPILOT: handle debug-button press + display-mode transitions on every
|
||||
# cycle — button edge events only live in the cycle's CS.buttonEvents and
|
||||
# would otherwise be dropped on skipped cycles.
|
||||
self.handle_screen_mode(CS)
|
||||
self.update_events(CS)
|
||||
self.update_frogpilot_events(CS)
|
||||
self.update_clearpilot_events(CS)
|
||||
|
||||
# CLEARPILOT: in park, only run the full step() every 10th cycle. data_sample
|
||||
# above still runs (CAN parse, button detection). Below, card.controls_update
|
||||
# still runs (CAN TX heartbeat, counters increment). The skipped outer logic
|
||||
# (events, state machine, PID/MPC, publishing) causes at most ~100ms lag on
|
||||
# button→state transitions, which is fine in park. Cached message bytes are
|
||||
# re-sent so downstream consumers see steady 100Hz.
|
||||
parked = CS.gearShifter == car.CarState.GearShifter.park
|
||||
full_cycle = (not parked) or (self.sm.frame % 10 == 0) or (self._cached_controlsState_bytes is None)
|
||||
cloudlog.timestamp("Events updated")
|
||||
|
||||
if full_cycle:
|
||||
self.update_events(CS)
|
||||
self.update_frogpilot_events(CS)
|
||||
self.update_clearpilot_events(CS)
|
||||
if not self.CP.passive and self.initialized:
|
||||
# Update control state
|
||||
self.state_transition(CS)
|
||||
|
||||
cloudlog.timestamp("Events updated")
|
||||
# Compute actuators (runs PID loops and lateral MPC)
|
||||
CC, lac_log = self.state_control(CS)
|
||||
CC = self.clearpilot_state_control(CC, CS)
|
||||
|
||||
if not self.CP.passive and self.initialized:
|
||||
# Update control state
|
||||
self.state_transition(CS)
|
||||
# Publish data
|
||||
self.publish_logs(CS, start_time, CC, lac_log)
|
||||
|
||||
# Compute actuators (runs PID loops and lateral MPC)
|
||||
CC, lac_log = self.state_control(CS)
|
||||
CC = self.clearpilot_state_control(CC, CS)
|
||||
|
||||
# Publish data (also sends CAN TX via card.controls_update inside)
|
||||
self.publish_logs(CS, start_time, CC, lac_log)
|
||||
|
||||
self.CS_prev = CS
|
||||
|
||||
# Update FrogPilot variables
|
||||
self.update_frogpilot_variables(CS)
|
||||
else:
|
||||
# CAN TX heartbeat: keep counters incrementing and CAN frames flowing to the car
|
||||
if not self.CP.passive and self.initialized:
|
||||
self.card.controls_update(self.CC, self.frogpilot_variables)
|
||||
# Re-publish cached messages so downstream freq_ok checks don't trip
|
||||
self.pm.send('controlsState', self._cached_controlsState_bytes)
|
||||
self.pm.send('carControl', self._cached_carControl_bytes)
|
||||
self.CS_prev = CS
|
||||
self.CS_prev = CS
|
||||
|
||||
# Update FrogPilot variables
|
||||
self.update_frogpilot_variables(CS)
|
||||
|
||||
def data_sample(self):
|
||||
"""Receive data from sockets and update carState"""
|
||||
@@ -499,18 +454,7 @@ class Controls:
|
||||
standby_ts = float(self.params_memory.get("ModelStandbyTs") or "0")
|
||||
except (ValueError, TypeError):
|
||||
standby_ts = 0
|
||||
now = time.monotonic()
|
||||
model_suppress = (now - standby_ts) < 2.0
|
||||
|
||||
# CLEARPILOT: suppress commIssue/location/params errors for 2s after lat was requested
|
||||
# Model FPS jumps from 4 to 20 on engage, causing transient freq check failures
|
||||
# lat_requested_ts is set in state_control on the False->True transition of LatRequested
|
||||
# CLEARPILOT: 2s window — long enough to cover the modeld rate transition burst
|
||||
# without hiding real comms issues. Extending this further risks masking a genuine
|
||||
# fault that demands immediate driver takeover.
|
||||
lat_engage_suppress = (now - self.lat_requested_ts) < 2.0
|
||||
|
||||
# CLEARPILOT: expose suppress flags + standby_ts for the debug dumper in step()
|
||||
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):
|
||||
@@ -524,14 +468,9 @@ class Controls:
|
||||
self.events.add(EventName.cameraMalfunction)
|
||||
elif not self.sm.all_freq_ok(self.camera_packets):
|
||||
self.events.add(EventName.cameraFrameRate)
|
||||
# CLEARPILOT: alert only when avg cycle time exceeds 20ms (≈50Hz effective).
|
||||
# Stock rk.lagging fires at 11.1ms (90% of 10ms) which the Hyundai CAN load
|
||||
# routinely crosses while driving — that's normal, not a fault. 50Hz control is
|
||||
# still plenty responsive. `rk.lagging` is still used defensively elsewhere
|
||||
# (lines ~479, 492) to skip secondary checks when slightly overloaded.
|
||||
avg_dt = sum(self.rk._dts) / len(self.rk._dts)
|
||||
alert_lagging = avg_dt > 0.020
|
||||
if not REPLAY and alert_lagging and not model_suppress and not lat_engage_suppress:
|
||||
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 not model_suppress and (len(self.sm['radarState'].radarErrors) or (not self.rk.lagging and not self.sm.all_checks(['radarState']))):
|
||||
@@ -546,60 +485,37 @@ 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)
|
||||
# CLEARPILOT: commIssue fires ONLY when messages actually aren't flowing. If all
|
||||
# subscribers are alive and CAN isn't timing out, comms are fine — any `valid=False`
|
||||
# is an upstream self-declaration that cascades from the MSGQ conflate +
|
||||
# slow-polling-consumer freq_ok measurement artifact. The car drives correctly
|
||||
# during these cascades (content is still usable), so we stop raising the banner.
|
||||
# Genuine comms failures — missing messages or CAN RX timeout — still fire.
|
||||
# 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
|
||||
commissue_cond = comms_really_broken and no_system_errors and not model_suppress and not lat_engage_suppress
|
||||
if commissue_cond:
|
||||
self._hyst_commissue += 1
|
||||
else:
|
||||
self._hyst_commissue = 0
|
||||
if self._hyst_commissue >= self.HYST_CYCLES:
|
||||
self.events.add(EventName.commIssue)
|
||||
if commissue_cond:
|
||||
# Log ONCE per entry into comms-really-broken state — no hyst counter so it doesn't
|
||||
# spam every cycle (hyst changes every cycle and would make logs dict always differ).
|
||||
if comms_really_broken and no_system_errors and not model_suppress:
|
||||
if not self.sm.all_alive():
|
||||
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],
|
||||
'not_alive': [s for s, alive in self.sm.alive.items() if not alive],
|
||||
'not_freq_ok': [s for s, freq_ok in self.sm.freq_ok.items() if not freq_ok],
|
||||
'can_rcv_timeout': self.card.can_rcv_timeout,
|
||||
}
|
||||
if logs != self.logged_comm_issue:
|
||||
import sys, json
|
||||
print("CLP commIssue " + json.dumps(logs), file=sys.stderr, flush=True)
|
||||
cloudlog.event("commIssue", error=True, **logs)
|
||||
self.logged_comm_issue = logs
|
||||
else:
|
||||
self.logged_comm_issue = None
|
||||
|
||||
if not (self.CP.notCar and self.joystick_mode):
|
||||
# CLEARPILOT: suppress locationd/paramsd "temporary error" when we're in the
|
||||
# freq-only cascade (same root as the commIssue suppression above). These events
|
||||
# cascade from upstream freq_ok artifacts — locationd's filterInitialized latches
|
||||
# False if calibrationd tips invalid due to freq, which cascades here.
|
||||
# Real failures still fire commIssue above via alive/valid/can_rcv_timeout gate.
|
||||
freq_only_cascade = (not self.sm.all_checks()) and not comms_really_broken
|
||||
|
||||
# deviceFalling is real-physics (shock sensor), no hysteresis, no cascade suppression.
|
||||
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)
|
||||
|
||||
posenet_bad = not self.sm['liveLocationKalman'].posenetOK and not model_suppress and not lat_engage_suppress and not freq_only_cascade
|
||||
self._hyst_posenet = self._hyst_posenet + 1 if posenet_bad else 0
|
||||
if self._hyst_posenet >= self.HYST_CYCLES:
|
||||
self.events.add(EventName.posenetInvalid)
|
||||
|
||||
locd_bad = not self.sm['liveLocationKalman'].inputsOK and not model_suppress and not lat_engage_suppress and not freq_only_cascade
|
||||
self._hyst_locationd_tmp = self._hyst_locationd_tmp + 1 if locd_bad else 0
|
||||
if self._hyst_locationd_tmp >= self.HYST_CYCLES:
|
||||
if not self.sm['liveLocationKalman'].inputsOK and not model_suppress:
|
||||
self.events.add(EventName.locationdTemporaryError)
|
||||
|
||||
paramsd_bad = not self.sm['liveParameters'].valid and not TESTING_CLOSET and (not SIMULATION or REPLAY) and not model_suppress and not lat_engage_suppress and not freq_only_cascade
|
||||
self._hyst_paramsd_tmp = self._hyst_paramsd_tmp + 1 if paramsd_bad else 0
|
||||
if self._hyst_paramsd_tmp >= self.HYST_CYCLES:
|
||||
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
|
||||
@@ -613,8 +529,16 @@ class Controls:
|
||||
if self.cruise_mismatch_counter > int(6. / DT_CTRL):
|
||||
self.events.add(EventName.cruiseMismatch)
|
||||
|
||||
# CLEARPILOT: FCW disabled — car's own radar AEB works better and triggers reliably.
|
||||
# The comma FCW was producing false positives and adds nothing over the stock system.
|
||||
# Check for FCW
|
||||
stock_long_is_braking = self.enabled and not self.CP.openpilotLongitudinalControl and CS.aEgo < -1.25
|
||||
model_fcw = self.sm['modelV2'].meta.hardBrakePredicted and not CS.brakePressed and not stock_long_is_braking
|
||||
planner_fcw = self.sm['longitudinalPlan'].fcw and self.enabled
|
||||
if planner_fcw or model_fcw:
|
||||
self.events.add(EventName.fcw)
|
||||
# self.fcw_random_event_triggered = True
|
||||
# elif self.fcw_random_event_triggered and self.random_events:
|
||||
# self.events.add(EventName.yourFrogTriedToKillMe)
|
||||
# self.fcw_random_event_triggered = False
|
||||
|
||||
for m in messaging.drain_sock(self.log_sock, wait_for_one=False):
|
||||
try:
|
||||
@@ -741,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)
|
||||
@@ -762,19 +710,8 @@ class Controls:
|
||||
|
||||
# Check which actuators can be enabled
|
||||
standstill = CS.vEgo <= max(self.CP.minSteerSpeed, MIN_LATERAL_CONTROL_SPEED) or CS.standstill
|
||||
# CLEARPILOT: lat_requested ignores momentary MDPS faults so modeld doesn't drop rate during
|
||||
# brief steerFaultTemporary blips (stale predictions on recovery caused a fault feedback loop).
|
||||
# lat_engaged gates the actual steering command and still respects the fault.
|
||||
lat_requested = (self.active or self.FPCC.alwaysOnLateral) and self.speed_check and \
|
||||
(not standstill or self.joystick_mode)
|
||||
lat_engaged = lat_requested and not CS.steerFaultTemporary and not CS.steerFaultPermanent
|
||||
|
||||
now_ts = time.monotonic()
|
||||
if lat_requested and not self.prev_lat_requested:
|
||||
self.lat_requested_ts = now_ts
|
||||
self.prev_lat_requested = lat_requested
|
||||
self.FPCC.latRequested = lat_requested
|
||||
CC.latActive = lat_engaged and (now_ts - self.lat_requested_ts) >= 0.25
|
||||
CC.latActive = (self.active or self.FPCC.alwaysOnLateral) and self.speed_check and not CS.steerFaultTemporary and not CS.steerFaultPermanent and \
|
||||
(not standstill or self.joystick_mode)
|
||||
CC.longActive = self.enabled and not self.events.contains(ET.OVERRIDE_LONGITUDINAL) and self.CP.openpilotLongitudinalControl
|
||||
|
||||
actuators = CC.actuators
|
||||
@@ -789,13 +726,13 @@ class Controls:
|
||||
CC.leftBlinker = model_v2.meta.laneChangeDirection == LaneChangeDirection.left
|
||||
CC.rightBlinker = model_v2.meta.laneChangeDirection == LaneChangeDirection.right
|
||||
|
||||
no_lat_lane_change = model_v2.meta.laneChangeState == LaneChangeState.laneChangeStarting and clearpilot_disable_lat_on_lane_change
|
||||
if no_lat_lane_change:
|
||||
if model_v2.meta.laneChangeState == LaneChangeState.laneChangeStarting and clearpilot_disable_lat_on_lane_change:
|
||||
CC.latActive = False
|
||||
# CLEARPILOT: noLatLaneChange now lives on frogpilotCarControl (see update_frogpilot_variables).
|
||||
# Also mirrored to frogpilot_variables so in-process carcontroller reads it without IPC.
|
||||
self.FPCC.noLatLaneChange = no_lat_lane_change
|
||||
self.frogpilot_variables.no_lat_lane_change = no_lat_lane_change
|
||||
self.frogpilot_variables.no_lat_lane_change = True
|
||||
self.FPCC.noLatLaneChange = True
|
||||
else:
|
||||
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
|
||||
@@ -1031,8 +968,6 @@ class Controls:
|
||||
controlsState.lateralControlState.torqueState = lac_log
|
||||
|
||||
self.pm.send('controlsState', dat)
|
||||
# CLEARPILOT: cache for re-publication on parked-skip cycles
|
||||
self._cached_controlsState_bytes = dat.to_bytes()
|
||||
|
||||
# onroadEvents - logged every second or on change
|
||||
if (self.sm.frame % int(1. / DT_CTRL) == 0) or (self.events.names != self.events_prev):
|
||||
@@ -1047,7 +982,6 @@ class Controls:
|
||||
cc_send.valid = CS.canValid
|
||||
cc_send.carControl = CC
|
||||
self.pm.send('carControl', cc_send)
|
||||
self._cached_carControl_bytes = cc_send.to_bytes()
|
||||
|
||||
# copy CarControl to pass to CarInterface on the next iteration
|
||||
self.CC = CC
|
||||
@@ -1078,11 +1012,6 @@ class Controls:
|
||||
while True:
|
||||
self.step()
|
||||
self.rk.monitor_time()
|
||||
# CLEARPILOT: accumulated debt from startup/blocking calls never recovers.
|
||||
# Reset the rate keeper when catastrophically behind — prevents a one-time
|
||||
# ~8s fingerprinting stall from poisoning the lag metric for the whole session.
|
||||
if self.rk.remaining < -1.0:
|
||||
self.rk._next_frame_time = time.monotonic() + self.rk._interval
|
||||
except SystemExit:
|
||||
e.set()
|
||||
t.join()
|
||||
@@ -1153,11 +1082,13 @@ class Controls:
|
||||
elif self.sm['modelV2'].meta.turnDirection == Desire.turnRight:
|
||||
self.events.add(EventName.turningRight)
|
||||
|
||||
# CLEARPILOT: check crash file once per second, not every cycle (stat syscall at 100Hz hurts)
|
||||
if self.sm.frame % 100 == 0:
|
||||
self._crashed_file_exists = os.path.isfile(os.path.join(sentry.CRASHES_DIR, 'error.txt'))
|
||||
if getattr(self, '_crashed_file_exists', False) and self.crashed_timer < 10:
|
||||
if os.path.isfile(os.path.join(sentry.CRASHES_DIR, 'error.txt')) and self.crashed_timer < 10:
|
||||
self.events.add(EventName.openpilotCrashed)
|
||||
|
||||
# if self.random_events and not self.openpilot_crashed_triggered:
|
||||
# self.events.add(EventName.openpilotCrashedRandomEvents)
|
||||
# self.openpilot_crashed_triggered = True
|
||||
|
||||
self.crashed_timer += DT_CTRL
|
||||
|
||||
# if self.speed_limit_alert or self.speed_limit_confirmation:
|
||||
@@ -1280,18 +1211,10 @@ class Controls:
|
||||
|
||||
self.FPCC.trafficModeActive = self.frogpilot_variables.traffic_mode and self.params_memory.get_bool("TrafficModeActive")
|
||||
|
||||
# CLEARPILOT: send only when any field changes, with 1Hz keepalive
|
||||
# Saves ~7ms/sec of capnp+zmq work (was sending 100Hz despite static contents)
|
||||
# latRequested and noLatLaneChange are edge-triggered too (rare flips)
|
||||
fpcc_tuple = (self.FPCC.alwaysOnLateral, self.FPCC.speedLimitChanged, self.FPCC.trafficModeActive,
|
||||
self.FPCC.latRequested, self.FPCC.noLatLaneChange)
|
||||
if fpcc_tuple != self._prev_fpcc or (self.sm.frame - self._last_fpcc_send_frame) >= 100:
|
||||
fpcc_send = messaging.new_message('frogpilotCarControl')
|
||||
fpcc_send.valid = CS.canValid
|
||||
fpcc_send.frogpilotCarControl = self.FPCC
|
||||
self.pm.send('frogpilotCarControl', fpcc_send)
|
||||
self._prev_fpcc = fpcc_tuple
|
||||
self._last_fpcc_send_frame = self.sm.frame
|
||||
fpcc_send = messaging.new_message('frogpilotCarControl')
|
||||
fpcc_send.valid = CS.canValid
|
||||
fpcc_send.frogpilotCarControl = self.FPCC
|
||||
self.pm.send('frogpilotCarControl', fpcc_send)
|
||||
|
||||
if self.params_memory.get_bool("FrogPilotTogglesUpdated"):
|
||||
self.update_frogpilot_params()
|
||||
@@ -1369,14 +1292,8 @@ class Controls:
|
||||
if any(be.pressed and be.type == FrogPilotButtonType.lkas for be in CS.buttonEvents):
|
||||
self.events.add(EventName.clpDebug)
|
||||
|
||||
def handle_screen_mode(self, CS):
|
||||
"""CLEARPILOT: tracks driving_gear, auto-resets display, and cycles
|
||||
ScreenDisplayMode on debug-button presses. Must run every cycle so button
|
||||
edge events aren't lost during parked-skip mode (edges happen in carstate
|
||||
edge detection and only appear in that one cycle's CS.buttonEvents)."""
|
||||
self.driving_gear = CS.gearShifter not in (GearShifter.neutral, GearShifter.park, GearShifter.reverse, GearShifter.unknown)
|
||||
|
||||
# auto-reset display when shifting into drive from screen-off
|
||||
def clearpilot_state_control(self, CC, CS):
|
||||
# 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)
|
||||
@@ -1395,9 +1312,8 @@ class Controls:
|
||||
|
||||
self.params_memory.put_int("ScreenDisplayMode", new_mode)
|
||||
|
||||
def clearpilot_state_control(self, CC, CS):
|
||||
|
||||
# ClearPilot speed processing (~2 Hz at 100 Hz 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']
|
||||
@@ -1410,7 +1326,6 @@ class Controls:
|
||||
cruise_standstill = CS.cruiseState.standstill
|
||||
self.speed_state.update(speed_ms, has_gps, speed_limit_ms, is_metric,
|
||||
cruise_speed_ms, cruise_active, cruise_standstill)
|
||||
|
||||
return CC
|
||||
|
||||
def main():
|
||||
|
||||
@@ -143,10 +143,6 @@ class LongitudinalPlanner:
|
||||
self.params = Params()
|
||||
self.params_memory = Params("/dev/shm/params")
|
||||
|
||||
# CLEARPILOT: track model FPS for dynamic dt adjustment
|
||||
self.model_fps = 20
|
||||
self._dbg_prev_valid = True # CLEARPILOT: diagnostic logging on valid transitions
|
||||
|
||||
self.radarless_model = self.params.get("Model", encoding='utf-8') in RADARLESS_MODELS
|
||||
|
||||
self.release = get_short_branch() == "clearpilot"
|
||||
@@ -177,19 +173,6 @@ class LongitudinalPlanner:
|
||||
return x, v, a, j
|
||||
|
||||
def update(self, sm):
|
||||
# CLEARPILOT: read actual model FPS and adjust dt accordingly
|
||||
model_fps_raw = self.params_memory.get("ModelFps")
|
||||
if model_fps_raw is not None:
|
||||
try:
|
||||
fps = int(model_fps_raw)
|
||||
if fps > 0 and fps != self.model_fps:
|
||||
self.model_fps = fps
|
||||
self.dt = 1.0 / fps
|
||||
self.v_desired_filter.dt = self.dt
|
||||
self.v_desired_filter.update_alpha(2.0) # rc=2.0, same as __init__
|
||||
except (ValueError, ZeroDivisionError):
|
||||
pass
|
||||
|
||||
self.mpc.mode = 'blended' if sm['controlsState'].experimentalMode else 'acc'
|
||||
|
||||
v_ego = sm['carState'].vEgo
|
||||
@@ -256,10 +239,7 @@ class LongitudinalPlanner:
|
||||
self.j_desired_trajectory = np.interp(ModelConstants.T_IDXS[:CONTROL_N], T_IDXS_MPC[:-1], self.mpc.j_solution)
|
||||
|
||||
# TODO counter is only needed because radar is glitchy, remove once radar is gone
|
||||
# CLEARPILOT: scale crash_cnt threshold by FPS — at 20fps need 3 frames (0.15s),
|
||||
# at 4fps need 1 frame (0.25s already exceeds 0.15s window)
|
||||
fcw_threshold = max(0, int(0.15 * self.model_fps) - 1) # 20fps->2, 4fps->0
|
||||
self.fcw = self.mpc.crash_cnt > fcw_threshold and not sm['carState'].standstill
|
||||
self.fcw = self.mpc.crash_cnt > 2 and not sm['carState'].standstill
|
||||
if self.fcw:
|
||||
cloudlog.info("FCW triggered")
|
||||
|
||||
@@ -278,18 +258,7 @@ class LongitudinalPlanner:
|
||||
def publish(self, sm, pm):
|
||||
plan_send = messaging.new_message('longitudinalPlan')
|
||||
|
||||
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 longitudinalPlan 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
|
||||
plan_send.valid = valid
|
||||
plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState'])
|
||||
|
||||
longitudinalPlan = plan_send.longitudinalPlan
|
||||
longitudinalPlan.modelMonoTime = sm.logMonoTime['modelV2']
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -285,11 +285,11 @@ def main() -> NoReturn:
|
||||
# 4Hz driven by cameraOdometry
|
||||
if sm.frame % 5 == 0:
|
||||
# CLEARPILOT: publish valid based on calibration status, not upstream sm.all_checks().
|
||||
# Original openpilot gated valid on fresh inputs, but that caused a cascade:
|
||||
# upstream freq glitches → liveCalibration.valid=False → locationd stays
|
||||
# uninitialized → paramsd fed garbage → bogus steerRatio/stiffnessFactor → erratic
|
||||
# steering. "valid" semantically means "calibration data is trustworthy"; that's a
|
||||
# question about calibration convergence, not input freshness.
|
||||
# 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)
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -187,7 +187,6 @@ def main():
|
||||
avg_offset_valid = True
|
||||
total_offset_valid = True
|
||||
roll_valid = True
|
||||
dbg_prev_valid = True # CLEARPILOT: track valid transitions
|
||||
|
||||
while True:
|
||||
sm.update()
|
||||
@@ -257,15 +256,7 @@ def main():
|
||||
liveParameters.filterState.std = P.tolist()
|
||||
liveParameters.filterState.valid = True
|
||||
|
||||
lp_valid = sm.all_checks()
|
||||
# CLEARPILOT: on transition to invalid, log per-subscriber state to paramsd.log
|
||||
if lp_valid != dbg_prev_valid and not lp_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 liveParameters valid=False: {' '.join(details)}", file=sys.stderr, flush=True)
|
||||
dbg_prev_valid = lp_valid
|
||||
msg.valid = lp_valid
|
||||
msg.valid = sm.all_checks()
|
||||
|
||||
if sm.frame % 1200 == 0: # once a minute
|
||||
params = {
|
||||
|
||||
@@ -226,8 +226,6 @@ def main(demo=False):
|
||||
with car.CarParams.from_bytes(params.get("CarParams", block=True)) as CP:
|
||||
estimator = TorqueEstimator(CP)
|
||||
|
||||
dbg_prev_valid = True # CLEARPILOT: track valid transitions
|
||||
|
||||
while True:
|
||||
sm.update()
|
||||
if sm.all_checks():
|
||||
@@ -238,15 +236,7 @@ def main(demo=False):
|
||||
|
||||
# 4Hz driven by liveLocationKalman
|
||||
if sm.frame % 5 == 0:
|
||||
tq_valid = sm.all_checks()
|
||||
# CLEARPILOT: log per-sub detail on transition to invalid — goes to torqued.log
|
||||
if tq_valid != dbg_prev_valid and not tq_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 liveTorqueParameters valid=False: {' '.join(details)}", file=sys.stderr, flush=True)
|
||||
dbg_prev_valid = tq_valid
|
||||
pm.send('liveTorqueParameters', estimator.get_msg(valid=tq_valid))
|
||||
pm.send('liveTorqueParameters', estimator.get_msg(valid=sm.all_checks()))
|
||||
|
||||
# Cache points every 60 seconds while onroad
|
||||
if sm.frame % 240 == 0:
|
||||
|
||||
@@ -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
|
||||
@@ -132,9 +132,10 @@ def main():
|
||||
pm = PubMaster(["driverStateV2"])
|
||||
|
||||
calib = np.zeros(CALIB_LEN, dtype=np.float32)
|
||||
# CLEARPILOT: cache last model output so we can republish (not re-infer) at standstill.
|
||||
# Saves ~7% CPU; downstream dmonitoringd sees a steady 10Hz stream with known-good
|
||||
# last readings (driver can't become distracted relative to a stopped car).
|
||||
# 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
|
||||
|
||||
@@ -147,16 +148,13 @@ def main():
|
||||
if sm.updated["liveCalibration"]:
|
||||
calib[:] = np.array(sm["liveCalibration"].rpyCalib)
|
||||
|
||||
standstill = sm["carState"].standstill
|
||||
|
||||
parked = sm["carState"].gearShifter == car.CarState.GearShifter.park
|
||||
t1 = time.perf_counter()
|
||||
if standstill and last_model_output is not None:
|
||||
# CLEARPILOT: reuse last inference at standstill
|
||||
model_output = last_model_output
|
||||
dsp_execution_time = 0.0
|
||||
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
|
||||
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))
|
||||
|
||||
+21
-32
@@ -183,9 +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
|
||||
model_standby = False
|
||||
last_standby_ts_write = 0
|
||||
params_memory = Params("/dev/shm/params")
|
||||
# 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
|
||||
@@ -241,31 +242,10 @@ def main(demo=False):
|
||||
|
||||
sm.update(0)
|
||||
|
||||
# CLEARPILOT: two-state modeld — 0fps only when parked (ignition-on means
|
||||
# engine running in park; ignition-off stops modeld at the manager level).
|
||||
# Standstill in drive (red light) keeps running so lateral stays responsive
|
||||
# and liveCalibration/paramsd observations continue.
|
||||
parked = sm['carState'].gearShifter == car.CarState.GearShifter.park
|
||||
should_standby = parked
|
||||
if should_standby and not model_standby:
|
||||
params_memory.put_bool("ModelStandby", True)
|
||||
params_memory.put("ModelFps", "0")
|
||||
model_standby = True
|
||||
cloudlog.warning("modeld: standby ON")
|
||||
elif not should_standby and model_standby:
|
||||
params_memory.put_bool("ModelStandby", False)
|
||||
params_memory.put("ModelFps", "20")
|
||||
model_standby = False
|
||||
run_count = 0
|
||||
frame_dropped_filter.x = 0.
|
||||
cloudlog.warning("modeld: standby OFF")
|
||||
if model_standby:
|
||||
now = _time.monotonic()
|
||||
if now - last_standby_ts_write > 1.0:
|
||||
params_memory.put("ModelStandbyTs", str(now))
|
||||
last_standby_ts_write = now
|
||||
last_vipc_frame_id = meta_main.frame_id
|
||||
continue
|
||||
# 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
|
||||
@@ -338,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,
|
||||
|
||||
@@ -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 324–384 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)
|
||||
Reference in New Issue
Block a user