3 Commits

Author SHA1 Message Date
brianhansonxyz 2ce6e8fe0c modelrevert: boot fixes + strip remaining variable-rate cruft
controlsd:
- stop writing the removed 'no_lat_lane_change' persistent param
  (key isn't in the whitelist anymore). Write to
  frogpilot_variables.no_lat_lane_change instead so the carcontroller
  read still works and the lane-change-disengage feature is preserved.
- initialize self.driving_gear = False in __init__; it's set at the
  end of step() in update_frogpilot_variables but clearpilot_state_control
  reads it on the first iteration before that's run.

modeld:
- remove the remaining CLEARPILOT variable-rate / standby code path
  that was baked into the initial-import commit (the one our first
  commit captured). Constant 20fps, no ModelStandby/ModelStandbyTs
  writes, no params_memory reads for 'no_lat_lane_change' (which
  isn't registered anymore).
- drop now-unused locals (model_standby, last_standby_ts_write,
  params_memory).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:42:11 -05:00
brianhansonxyz 3bd0c942e8 controlsd: re-apply QoL hooks on top of reverted baseline
Re-adds the non-self-drive controlsd integrations that the UI and
memory-param pipeline need:

- import SpeedState from clearpilot.speed_logic
- self.speed_state, speed_state_frame, was_driving_gear init
- subscribe to gpsLocation (ignored in alive/freq_ok/valid gates — OK if
  the GPS daemon isn't publishing)
- clearpilot_state_control:
  - auto-reset ScreenDisplayMode 3→0 on park→drive transition
  - ~2Hz speed-state update driving the speed-limit sign and cruise
    over/under warning sign via ClearpilotCruiseWarning /
    ClearpilotSpeedLimitDisplay memory params

The debug-button (LFA) ScreenDisplayMode cycling already lived in the
reverted baseline (it was in the first commit), so it's preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 13:31:51 -05:00
brianhansonxyz 12da9acfdd revert modeld/controlsd/plannerd/locationd to first-commit baseline
Starting point for rebuilding self-drive from a known-good baseline.
Reverts the following to their state at f46339c:
- selfdrive/modeld/modeld.py (constant 20fps, no variable-rate / standby skip)
- selfdrive/modeld/dmonitoringmodeld.py (no carState sub, no standstill skip)
- selfdrive/controls/controlsd.py (no parked-cycle skip, no FPCC hoisting, no MDPS split)
- selfdrive/controls/lib/longitudinal_planner.py
- selfdrive/locationd/calibrationd.py (valid = sm.all_checks again)
- selfdrive/locationd/paramsd.py
- selfdrive/locationd/torqued.py

All non-self-drive features (thermald fan control, speed limit controller,
cruise warning signs, UI state transitions, GPS fixes, ClearPilot menu,
dashcamd, telemetry, etc.) remain as-is on this branch — only the 4 core
self-drive processes are reverted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 13:26:36 -05:00
10 changed files with 22 additions and 277 deletions
+4 -9
View File
@@ -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,
+3 -5
View File
@@ -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.
+5 -34
View File
@@ -485,16 +485,13 @@ 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: 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_checks() or self.card.can_rcv_timeout) 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
elif not self.sm.all_freq_ok():
self.events.add(EventName.commIssueAvgFreq)
else: # invalid or can_rcv_timeout.
self.events.add(EventName.commIssue)
logs = {
'invalid': [s for s, valid in self.sm.valid.items() if not valid],
@@ -665,30 +662,6 @@ 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)
@@ -729,10 +702,8 @@ class Controls:
if model_v2.meta.laneChangeState == LaneChangeState.laneChangeStarting and clearpilot_disable_lat_on_lane_change:
CC.latActive = False
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
-4
View File
@@ -34,10 +34,6 @@ 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)
+1 -3
View File
@@ -85,9 +85,7 @@ def frogpilot_thread():
frogpilot_planner = FrogPilotPlanner(CP)
frogpilot_planner.update_frogpilot_params()
# CLEARPILOT: skip planner work while parked.
parked = sm['carState'].gearShifter == car.CarState.GearShifter.park
if sm.updated['modelV2'] and not parked:
if sm.updated['modelV2']:
frogpilot_planner.update(sm['carState'], sm['controlsState'], sm['frogpilotCarControl'], sm['frogpilotNavigation'],
sm['liveLocationKalman'], sm['modelV2'], sm['radarState'])
frogpilot_planner.publish(sm, pm)
+1 -8
View File
@@ -284,14 +284,7 @@ def main() -> NoReturn:
# 4Hz driven by cameraOdometry
if sm.frame % 5 == 0:
# 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)
calibrator.send_data(pm, sm.all_checks())
if __name__ == "__main__":
+1 -7
View File
@@ -308,18 +308,12 @@ 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 (clearpilot_disable_gps || !log.getHasFix() || gps_unreasonable || gps_accuracy_insane || gps_lat_lng_alt_insane || gps_vel_insane) {
if (!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;
+3 -13
View File
@@ -7,7 +7,7 @@ import ctypes
import numpy as np
from pathlib import Path
from cereal import car, messaging
from cereal import messaging
from cereal.messaging import PubMaster, SubMaster
from cereal.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
from openpilot.common.swaglog import cloudlog
@@ -128,15 +128,10 @@ def main():
assert vipc_client.is_connected()
cloudlog.warning(f"connected with buffer size: {vipc_client.buffer_len}")
sm = SubMaster(["liveCalibration", "carState"])
sm = SubMaster(["liveCalibration"])
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:
@@ -148,13 +143,8 @@ def main():
if sm.updated["liveCalibration"]:
calib[:] = np.array(sm["liveCalibration"].rpyCalib)
parked = sm["carState"].gearShifter == car.CarState.GearShifter.park
t1 = time.perf_counter()
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)
model_output, dsp_execution_time = model.run(buf, calib)
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))
+4 -17
View File
@@ -183,10 +183,6 @@ 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
@@ -318,21 +314,12 @@ def main(demo=False):
**({'radar_tracks': radar_tracks,} if DISABLE_RADAR else {}),
}
# 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
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,
@@ -1,177 +0,0 @@
# 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)