From 0dc8002c6d65edd64deb6ff89d3f4c1b8dcfa4a2 Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Sun, 26 Apr 2026 13:18:46 -0500 Subject: [PATCH] revert hyundai canfd torque to stock; park-publish keepalive; dashcam idle on park MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three independent changes bundled together. Revert hyundai canfd steering torque limits to comma stock defaults (270 / 2 / 3 / 112) in both panda safety and openpilot CarControllerParams. The 270->324 bump caused overcorrection on turns and weaving on straights. Web research turned up no public reports of any 4th gen Tucson NX4 owner bumping STEER_MAX — the documented Tucson tuning effort is entirely on lateralTuning (latAccelFactor ~2.9-3.1, friction ~0.095), not the cap. hoomoose's EV6/Ioniq 5 PR #25723 is the canonical "raise STEER_MAX without dropping latAccelFactor causes overcorrection" data point — and even that change was reverted upstream. Right next move for this car is to tune latAccelFactor / friction, not the torque ceiling. plannerd: keep publishing longitudinalPlan at the normal cadence in park, but skip update() compute. Skipping publishes entirely caused longitudinalPlan to time out the alive flag at controlsd, which fired a real commIssue ("not_alive") on park->drive. Stale published values are fine because controlsd's own park short-circuit ignores the longitudinalPlan content while parked. Also gate publish_ui_plan on not-parked: it reads longitudinal_planner.a_desired_trajectory_full which is only set inside update(), so calling it without a prior update crashes plannerd with AttributeError (which fires "Process Not Running" on screen). uiPlan is UI-only, not on controlsd's commIssue check list, so going silent in park is fine. frogpilot_process: same idea — keep publishing frogpilotPlan in park to keep alive, skip the heavy update() compute. dashcamd: close the trip immediately on gear shift to PARK (was: 10-min idle timer before close). User wants the dashcam idle in park and a fresh trip on every drive engagement; brief drive-thru / fuel-stop across-trip continuity isn't valued. Co-Authored-By: Claude Opus 4.7 (1M context) --- panda/board/safety/safety_hyundai_canfd.h | 13 ++++--------- selfdrive/car/hyundai/values.py | 8 +++----- selfdrive/clearpilot/dashcamd.cc | 23 ++++++++++++----------- selfdrive/controls/plannerd.py | 20 ++++++++++++++------ selfdrive/frogpilot/frogpilot_process.py | 12 ++++++++---- 5 files changed, 41 insertions(+), 35 deletions(-) diff --git a/panda/board/safety/safety_hyundai_canfd.h b/panda/board/safety/safety_hyundai_canfd.h index e82dc60..1a1acdc 100755 --- a/panda/board/safety/safety_hyundai_canfd.h +++ b/panda/board/safety/safety_hyundai_canfd.h @@ -1,16 +1,11 @@ #include "safety_hyundai_common.h" const SteeringLimits HYUNDAI_CANFD_STEERING_LIMITS = { - // 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_steer = 270, + .max_rt_delta = 112, .max_rt_interval = 250000, - .max_rate_up = 3, - .max_rate_down = 5, + .max_rate_up = 2, + .max_rate_down = 3, .driver_torque_allowance = 250, .driver_torque_factor = 2, .type = TorqueDriverLimited, diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 53f1850..915a581 100755 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -27,14 +27,12 @@ class CarControllerParams: self.STEER_STEP = 1 # 100 Hz if CP.carFingerprint in CANFD_CAR: - # 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_MAX = 270 self.STEER_DRIVER_ALLOWANCE = 250 self.STEER_DRIVER_MULTIPLIER = 2 self.STEER_THRESHOLD = 250 - self.STEER_DELTA_UP = 3 - self.STEER_DELTA_DOWN = 5 + self.STEER_DELTA_UP = 2 + self.STEER_DELTA_DOWN = 3 # 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. diff --git a/selfdrive/clearpilot/dashcamd.cc b/selfdrive/clearpilot/dashcamd.cc index 97f84fa..0e4fdd5 100644 --- a/selfdrive/clearpilot/dashcamd.cc +++ b/selfdrive/clearpilot/dashcamd.cc @@ -12,20 +12,20 @@ * Trip lifecycle state machine: * * WAITING: - * - Process starts in this state + * - Process starts in this state. Idle. * - Waits for valid system time (year >= 2024) AND car in drive gear * - Transitions to RECORDING when both conditions met * * RECORDING: * - Actively encoding frames, car is in drive - * - Car leaves drive → start 10-min idle timer → IDLE_TIMEOUT - * - * IDLE_TIMEOUT: - * - Car left drive, still recording with timer running - * - Car re-enters drive → cancel timer → RECORDING - * - Timer expires → close trip → WAITING + * - Gear shift into PARK → close trip immediately → WAITING (idle) * - Ignition off → close trip → WAITING * + * IDLE_TIMEOUT (deprecated, retained for safety): + * - Was used to keep recording across brief drive-thru / fuel stops. + * - Now unreachable: drive→park transitions close the trip immediately + * and a fresh trip starts on the next drive engagement. + * * Graceful shutdown (DashcamShutdown param): * - thermald sets DashcamShutdown="1" before device power-off * - dashcamd closes current segment, acks, exits @@ -301,10 +301,11 @@ int main(int argc, char *argv[]) { } case RECORDING: - if (!in_drive) { - idle_timer_start = now; - state = IDLE_TIMEOUT; - LOGW("dashcamd: car left drive, starting 10-min idle timer"); + // CLEARPILOT: close trip immediately on park (no idle timer). User wants + // dashcam idle in park, fresh trip on each drive engagement. + if (gear == cereal::CarState::GearShifter::PARK) { + LOGW("dashcamd: gear in park, closing trip"); + close_trip(); } break; diff --git a/selfdrive/controls/plannerd.py b/selfdrive/controls/plannerd.py index 6906fdf..94770c6 100755 --- a/selfdrive/controls/plannerd.py +++ b/selfdrive/controls/plannerd.py @@ -34,13 +34,21 @@ 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) + # CLEARPILOT: skip the planning compute while parked, but KEEP publishing + # at the normal cadence so consumers' alive flags stay healthy. Skipping + # publishes entirely caused longitudinalPlan to go alive=False at + # controlsd, which fires a real commIssue the moment we shift out of park. + # Stale published values are fine — controlsd's own park short-circuit + # ignores the longitudinalPlan content while parked anyway. + parked = sm['carState'].gearShifter == car.CarState.GearShifter.park + if not parked: + longitudinal_planner.update(sm) longitudinal_planner.publish(sm, pm) - publish_ui_plan(sm, pm, longitudinal_planner) + # publish_ui_plan reads longitudinal_planner.a_desired_trajectory_full + # which is only set inside update(). Skip it while parked — uiPlan is + # UI-only, not on controlsd's commIssue check list, so going silent is fine. + if not parked: + publish_ui_plan(sm, pm, longitudinal_planner) def main(): plannerd_thread() diff --git a/selfdrive/frogpilot/frogpilot_process.py b/selfdrive/frogpilot/frogpilot_process.py index 65b4667..fbe3d3b 100755 --- a/selfdrive/frogpilot/frogpilot_process.py +++ b/selfdrive/frogpilot/frogpilot_process.py @@ -85,11 +85,15 @@ def frogpilot_thread(): frogpilot_planner = FrogPilotPlanner(CP) frogpilot_planner.update_frogpilot_params() - # CLEARPILOT: skip planner work while parked. + # CLEARPILOT: skip planner compute while parked, but KEEP publishing at + # the normal cadence so frogpilotPlan stays alive at consumers. Skipping + # publishes entirely caused commIssue ("not_alive: frogpilotPlan") at + # controlsd the moment we shifted out of park. 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']) + if sm.updated['modelV2']: + if not parked: + frogpilot_planner.update(sm['carState'], sm['controlsState'], sm['frogpilotCarControl'], sm['frogpilotNavigation'], + sm['liveLocationKalman'], sm['modelV2'], sm['radarState']) frogpilot_planner.publish(sm, pm) if not time_validated: