Compare commits

...

10 Commits

Author SHA1 Message Date
prabhaavp 98f8443122 Update DeepWiki badge in README.md 2026-10-26 16:57:34 -04:00
firestar5683 ad2bb3a212 Update StarPilotHQ Links 2026-10-26 15:48:31 -05:00
firestar5683 a25611435c Trick or Treat 2026-10-26 07:35:31 -05:00
firestar5683 823a5d5674 Waffles 2026-10-15 18:09:34 -05:00
Michael 1391d3a39c update README.md 2026-10-15 15:43:10 -07:00
Michael 807acc0522 update README.md 2026-10-15 15:32:22 -07:00
firestar5683 b220a37f4b Sea Turtles 2026-10-13 23:05:10 -05:00
firestar5683 3ccae7b88d build 2026-10-12 20:26:21 -05:00
firestar5683 88c73987d0 Green Garden 2026-10-12 19:32:28 -05:00
firestarsdog 83f914fc5c Disable random events on boot if enabled 2026-10-11 00:55:10 -04:00
122 changed files with 2696 additions and 145 deletions
+29 -26
View File
@@ -1,26 +1,19 @@
# StarPilot
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/firestar5683/StarPilot)
[![Ask DeepWiki](https://img.shields.io/badge/Ask-DeepWiki-blue?logo=readthedocs&logoColor=white)](https://deepwiki.com/StarPilotHQ/StarPilot)
[![Discord](https://img.shields.io/discord/1387432184121393333?label=Discord)](https://firestar.link/discord)
[![Last Updated](https://img.shields.io/github/last-commit/firestar5683/StarPilot/StarPilot)](https://github.com/firestar5683/StarPilot)
[![Last Updated](https://img.shields.io/github/last-commit/StarPilotHQ/StarPilot/StarPilot)](https://github.com/StarPilotHQ/StarPilot)
[![Wiki](https://img.shields.io/badge/Wiki-StarPilot-blue?logo=wiki)](https://wiki.firestar.link)
**StarPilot** is a custom fork of [comma.ai's openpilot](https://comma.ai/openpilot),
an open source driver assistance system.
Openpilot provides
* Automated Lane Centering
* Adaptive Cruise Control
* Lane Change Assist
* Driver Monitoring *without wheel nags*
StarPilot was formerly a GM targeted fork,
but [has expanded to offer Quality-Of-Life improvements for all](#features)!
StarPilot is built off of [FrogPilot](https://github.com/FrogAi/FrogPilot)
and supports the major features FrogPilot offers.
Ford-specific lateral-control and vehicle-support work includes substantial adaptations from
[BluePilot](https://github.com/BluePilotDev/bluepilot/tree/bp-7.0), principally developed by
[Alan Polk](https://github.com/alan-polk) with additional BluePilot contributors. StarPilot's
@@ -37,6 +30,7 @@ this adaptation. Detailed code lineage is recorded in [CREDITS.md](CREDITS.md#hy
StarPilot has a vibrant, welcoming community [discord](https://firestar.link/discord).
Stop by to chat or ask questions!
If your car isn't driving as well as you'd like, [open a ticket so we can help](https://firestar.link/logs)!
## Documentation
@@ -45,27 +39,35 @@ installation guides, and software configuration.
## Features
* Full support for Comma C3, C3X, and C4
* **Full support for Comma C3, C3X, and C4**
* Full support for chestnut/eGPU on C3, C3X, and C4. C3/C3X may require special gps handling.
* Model switcher with all of comma's tinygrad driving models
* Special longitudinal planner tuning for VoACC (visual only, radar-less) vehicles
* Custom-tuned torque controllers for an expanding list of cars.
* StarPilot custom torque controller
* StarPilot custom longitudinal planner for both radar and VoACC (visual only, radar-less) vehicles
* **Custom tuning for an expanding list of vehicles**
* Firestar Lateral Method -
Auto tuning system to adapt openpilot to *your* car's unique lateral characteristics.
* Galaxy: StarPilot's portal to configure your comma device using your phone from anywhere.
Download models, change settings, update software, visualize live model outputs for tuning.
* Always On Lateral (full time steering assist)*
* Speed Limit Controller*
* Learning Curve Speed Controller*
* Conditional Experimental Mode (CEM)*
* Driving Profiles*
* Custom themes*
* Alert Volume Controller*
* Comma Pedal Interceptor support*
* Toyota SDSU support*
* ZSS support*
* High quality dashcam recordings*
* Enhanced tuning for CEM (dynamic experimental mode switching)
Download models, change settings, update software, diagnostics, manage dashcam video, and more.
* Navigation - Display routes and give the driving model hints on taking exits and turns.
* Always On Lateral (full time steering assist)
* Lane Centering - Enable to center between detected lane lines,
with graceful fallback to model E2E lateral outputs and safety breakout
* Force Stop 2.0 - Better stops at stop signs and traffic lights.
* Speed Limit Controller - Use dashboard, map data, and/or vision detection for speed limit signs
* Curve Speed Controller - Learns from how you take curves
* Conditional Experimental Mode (CEM)
* Conditional Chill Mode (CCM)
* Driving Profiles and Traffic Mode
* Custom themes
* Alert volume customization
* Comma Pedal Interceptor support
* Bluetooth support - Connect keyboards, controllers, steering wheel remotes, and more.
* Sentry mode - Record when motion is detected while parked.
* V-ASM - Detect vehicles in the adjacent/blind spots using the driver-facing camera.
* Additional car support - Toyota SDSU, ZSS, CAN filter; pre-AP tesla; ford; Polestar 2; and more.
* And more!
\* [Inherited from FrogPilot](https://github.com/FrogAi/FrogPilot#openpilot-vs-frogpilot)
## GM-only Features
@@ -82,6 +84,7 @@ Download models, change settings, update software, visualize live model outputs
* Native and cross compilation for Windows, Mac, and Ubuntu
* Custom AGNOS to support C3, C3X, and C4
* Raylib UI on C3, C3X, and C4
* To run UI on PC:
* `./c3` for large UI
* `./c4` for small UI
+28
View File
@@ -28,6 +28,34 @@ public sunnypilot repositories:
[`f95f996f5917dcbbf2e32fe51b606a24cf836af6`](https://github.com/sunnypilot/opendbc/commit/f95f996f5917dcbbf2e32fe51b606a24cf836af6).
- Historical reconstruction and contributors: [CREDITS.md](CREDITS.md#hyundai-kia-and-genesis-support-adapted-from-sunnypilot)
## Continental ARS510 radar
The optional RAV4 radar decoders and `toyota_tss2_ars510.dbc` are adapted from
<https://github.com/ds-sebastian/ars510-radar> at
`fb5b29c2b5999f34afeb6f7ac494a73949d66c67`.
MIT License
Copyright (c) 2026 ARS510 radar contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## Applicable upstream license notices
The upstream repositories and branches identified above publish both `LICENSE` and `LICENSE.md`.
Binary file not shown.
+2
View File
@@ -631,6 +631,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ScreenBrightnessOnroadManual", {PERSISTENT, INT, "100", "100", 2, SETTINGS_SIMPLE}},
{"ScreenBrightnessOnroadOffset", {PERSISTENT, INT, "0", "0", 2, SETTINGS_SIMPLE}},
{"ScreenManagement", {PERSISTENT, BOOL, "1", "0", 1, SETTINGS_SIMPLE}},
{"KeepScreenOnGpuLoading", {PERSISTENT, BOOL, "0", "0", 1, SETTINGS_SIMPLE}},
{"ScreenRecorder", {PERSISTENT, BOOL, "1", "0", 2, SETTINGS_SIMPLE}},
{"ScreenTimeout", {PERSISTENT, INT, "30", "30", 2, SETTINGS_SIMPLE}},
{"ScreenTimeoutOnroad", {PERSISTENT, INT, "30", "10", 2, SETTINGS_SIMPLE}},
@@ -803,6 +804,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"GMAutoHold", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"VoltOnePedalMode", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"ToyotaAutoHold", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"ToyotaRadarMode", {PERSISTENT, INT, "0", "0", 2, SETTINGS_SIMPLE}},
{"WarningImmediateVolume", {PERSISTENT, INT, "101", "101", 2, SETTINGS_SIMPLE}},
{"WarningSoftVolume", {PERSISTENT, INT, "101", "101", 2, SETTINGS_SIMPLE}},
{"WeatherPresets", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
Binary file not shown.
+36
View File
@@ -0,0 +1,36 @@
# Experimental RAV4 Radar
The optional Continental ARS510 parser supports the Toyota RAV4 2022 and RAV4
2023-2025 profiles, including hybrids. Earlier RAV4s, RAV4 Prime, and other Toyota
or Lexus profiles are unchanged. This is a beta feature, disabled by default.
In Galaxy, open **Vehicle > RAV4 Radar (Experimental)** while parked:
- **Off** preserves the existing vision-only behavior on these profiles.
- **ACC Target** publishes the radar's selected lead. Other leads remain vision-based.
- **Fused Tracks** decodes up to 20 objects, filters speed uncertainty, uses the
radar's ACC target to anchor its matched object's speed and distance, and gates
young, distant, or off-path objects before publishing them.
Restart the device after changing the mode. The equivalent persistent parameter
is `ToyotaRadarMode`: `0` for Off, `1` for ACC Target, and `2` for Fused Tracks.
The parser waits through the radar's delayed startup rather than requiring its
object frames in the initial fingerprint. Once running, missing scans clear the
tracks and report radar unavailability; healthy tracks are not cleared between scans.
This setting does not enable Alpha Long, change longitudinal or lateral tuning,
send new CAN commands, or alter the existing radar-disable procedure. Without a
radar CAN filter, Alpha Long still disables the factory AEB output. Receiving
radar objects does not restore AEB. Filter-equipped and unfiltered cars retain
their existing longitudinal and safety configurations.
Use only for supervised beta testing and remain ready to brake or steer. An
available parser is not evidence that a particular car is providing live objects.
Collect rlogs with normal lead following, braking, stops and takeoffs, curves,
cut-ins, and both stock ACC and Alpha Long configurations. Bookmark unexpected
braking or lead loss. Do not deliberately approach an obstacle to test AEB.
The parser is adapted from the [ARS510 radar reference](https://github.com/ds-sebastian/ars510-radar)
at `fb5b29c2b5999f34afeb6f7ac494a73949d66c67`; attribution and its MIT notice are
preserved in `THIRD_PARTY_NOTICES.md`. Protocol and offline replay validation do
not establish closed-loop performance on every trim, firmware, or harness.
@@ -415,6 +415,18 @@ def supports_bolt_acc_pedal_friction_experiment(CP) -> bool:
)
def update_bolt_acc_pedal_stop_hold(CP, CC, CS, holding: bool) -> bool:
return (
supports_bolt_acc_pedal_friction_experiment(CP) and
CC.longActive and
CC.actuators.longControlState == LongCtrlState.stopping and
not CC.cruiseControl.resume and
not CS.out.gasPressed and
not CS.out.brakePressed and
(holding or CS.out.standstill)
)
def get_bolt_acc_pedal_friction_brake(apply_brake, full_brake_accel, v_ego, params) -> int:
if apply_brake <= 0:
return 0
@@ -583,6 +595,7 @@ class CarController(CarControllerBase):
self.gm_auto_hold_enabled = False
self.bolt_acc_pedal_friction_release_frames = 0
self.bolt_acc_pedal_friction_low_speed_active = False
self.bolt_acc_pedal_stop_hold = False
self.truck_friction_brake_active = False
self.truck_follow_accel = 0.0
@@ -772,6 +785,7 @@ class CarController(CarControllerBase):
actuators = CC.actuators
self.aego = CS.out.aEgo
accel = actuators.accel
self.bolt_acc_pedal_stop_hold = update_bolt_acc_pedal_stop_hold(self.CP, CC, CS, self.bolt_acc_pedal_stop_hold)
press_regen_paddle = False
auto_hold_enabled = supports_volt_auto_hold(self.CP, self.gm_auto_hold_enabled)
volt_one_pedal_supported = supports_volt_one_pedal(
@@ -1177,6 +1191,9 @@ class CarController(CarControllerBase):
else:
can_sends.append(gmcan.create_buttons(self.packer_pt, CanBus.POWERTRAIN, (CS.buttons_counter + 1) % 4, CruiseButtons.DECEL_SET))
if self.CP.enableGasInterceptorDEPRECATED:
if self.bolt_acc_pedal_stop_hold:
interceptor_gas_cmd = 0.0
self.pedal_steady = 0.0
can_sends.append(create_gas_interceptor_command(self.packer_pt, interceptor_gas_cmd, idx))
if bolt_acc_pedal_friction_experiment:
friction_brake_bus = get_friction_brake_bus(self.CP)
@@ -70,6 +70,7 @@ from opendbc.car.gm.carcontroller import (
supports_bolt_acc_pedal_friction_experiment,
supports_volt_auto_hold,
supports_volt_one_pedal,
update_bolt_acc_pedal_stop_hold,
use_interceptor_sng_launch,
)
from opendbc.car.gm.gmcan import get_friction_brake_mode
@@ -314,6 +315,150 @@ def test_fixed_stopping_brake_stays_enabled_for_normal_acc_path():
assert not should_use_fixed_stopping_brake(CP, True, True, True)
def _bolt_stop_hold_case():
CP = SimpleNamespace(
carFingerprint=CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL,
openpilotLongitudinalControl=True,
enableGasInterceptorDEPRECATED=True,
flags=GMFlags.PEDAL_LONG.value,
)
CC = SimpleNamespace(
longActive=True,
actuators=SimpleNamespace(longControlState=structs.CarControl.Actuators.LongControlState.stopping),
cruiseControl=SimpleNamespace(resume=False),
)
CS = SimpleNamespace(out=SimpleNamespace(standstill=False, gasPressed=False, brakePressed=False))
return CP, CC, CS
def test_bolt_pedal_stop_hold_preserves_approach_until_standstill():
CP, CC, CS = _bolt_stop_hold_case()
assert not update_bolt_acc_pedal_stop_hold(CP, CC, CS, False)
CS.out.standstill = True
assert update_bolt_acc_pedal_stop_hold(CP, CC, CS, False)
def test_bolt_pedal_stop_hold_stays_latched_when_grade_creep_clears_standstill():
CP, CC, CS = _bolt_stop_hold_case()
CS.out.standstill = True
holding = update_bolt_acc_pedal_stop_hold(CP, CC, CS, False)
CS.out.standstill = False
assert update_bolt_acc_pedal_stop_hold(CP, CC, CS, holding)
@pytest.mark.parametrize("release", ["long_off", "pid", "starting", "off", "resume", "gas", "brake"])
def test_bolt_pedal_stop_hold_releases_for_launch_or_driver_takeover(release):
CP, CC, CS = _bolt_stop_hold_case()
CS.out.standstill = True
if release == "long_off":
CC.longActive = False
elif release in ("pid", "starting", "off"):
CC.actuators.longControlState = getattr(structs.CarControl.Actuators.LongControlState, release)
elif release == "resume":
CC.cruiseControl.resume = True
elif release == "gas":
CS.out.gasPressed = True
else:
CS.out.brakePressed = True
assert not update_bolt_acc_pedal_stop_hold(CP, CC, CS, True)
@pytest.mark.parametrize("platform", [
CAR.CHEVROLET_BOLT_ACC_2022_2023,
CAR.CHEVROLET_BOLT_CC_2022_2023,
CAR.CHEVROLET_BOLT_CC_2018_2021,
CAR.CHEVROLET_BOLT_CC_2017,
CAR.CHEVROLET_VOLT,
CAR.CHEVROLET_MALIBU_HYBRID_CC,
CAR.CHEVROLET_SILVERADO,
])
def test_bolt_pedal_stop_hold_does_not_change_other_profiles(platform):
CP, CC, CS = _bolt_stop_hold_case()
CP.carFingerprint = platform
CS.out.standstill = True
assert not update_bolt_acc_pedal_stop_hold(CP, CC, CS, True)
@pytest.mark.parametrize("disabled", ["long", "pedal", "flag"])
def test_bolt_pedal_stop_hold_requires_active_pedal_long_configuration(disabled):
CP, CC, CS = _bolt_stop_hold_case()
CS.out.standstill = True
if disabled == "long":
CP.openpilotLongitudinalControl = False
elif disabled == "pedal":
CP.enableGasInterceptorDEPRECATED = False
else:
CP.flags = 0
assert not update_bolt_acc_pedal_stop_hold(CP, CC, CS, True)
def test_bolt_pedal_sender_zeros_stopped_offset_without_changing_brake_fade(monkeypatch):
import opendbc.car.gm.carcontroller as controller_module
monkeypatch.setattr(controller_module, "Params", lambda: SimpleNamespace(get_bool=lambda _key: False, get=lambda _key: None))
CP = structs.CarParams()
CP.carFingerprint = CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL
CP.openpilotLongitudinalControl = True
CP.enableGasInterceptorDEPRECATED = True
CP.flags = GMFlags.PEDAL_LONG.value
CP.networkLocation = CarParams.NetworkLocation.fwdCamera
CP.transmissionType = CarParams.TransmissionType.direct
CP.mass = 1669.0
CP.wheelbase = 2.63779
CP.vEgoStarting = 0.35
CP.vEgoStopping = 0.25
CP.stopAccel = -0.25
CP.radarUnavailable = True
CP.longitudinalTuning.kpBP = [0.0]
CP.longitudinalTuning.kpV = [0.095]
CP.longitudinalTuning.kiBP = [0.0]
CP.longitudinalTuning.kiV = [0.07]
controller = CarController({}, CP)
controller.frame = 0
state = structs.CarState()
state.vEgo = 0.2
state.gearShifter = structs.CarState.GearShifter.drive
state.cruiseState.available = True
CS = SimpleNamespace(
out=state.as_reader(), auto_hold_armed=False, auto_hold_engaged=False, auto_hold_drive_time=0.0,
one_pedal_drive_time=0.0, regen_release_timer=0.0, loopback_lka_steering_cmd_updated=False,
loopback_lka_steering_cmd_ts_nanos=0, cam_lka_steering_cmd_counter=0, pt_lka_steering_cmd_counter=0,
pcm_acc_status=AccState.OFF, buttons_counter=0, cruise_buttons=0,
pscm_status={key: 0 for key in ("HandsOffSWDetectionMode", "HandsOffSWlDetectionStatus", "LKATorqueDeliveredStatus",
"LKADriverAppldTrq", "LKATorqueDelivered", "LKATotalTorqueDelivered", "RollingCounter", "PSCMStatusChecksum")},
)
control = structs.CarControl()
control.enabled = True
control.longActive = True
control.actuators.accel = -0.253
control.actuators.longControlState = structs.CarControl.Actuators.LongControlState.stopping
toggles = SimpleNamespace(volt_sng=False)
def send():
controller.frame = (controller.frame + 3) // 4 * 4
_, frames = controller.update(control.as_reader(), CS, 2_000_000_000 + controller.frame * 10_000_000, toggles)
pedal = next(dat for address, dat, _bus in frames if address == 0x200)
brake = next(dat for address, dat, _bus in frames if address == 0x315)
assert brake[0] & 0xF == 0 and brake[1] == 0
return bool(pedal[4] & 0x80)
assert send()
state.vEgo = 0.05
state.standstill = True
CS.out = state.as_reader()
assert not send()
assert controller.pedal_steady == 0.0
state.vEgo = 0.244
state.standstill = False
CS.out = state.as_reader()
assert not send()
control.actuators.longControlState = structs.CarControl.Actuators.LongControlState.starting
control.actuators.accel = 0.55
assert send()
assert not controller.bolt_acc_pedal_stop_hold
def test_stock_cancel_is_suppressed_when_acc_is_faulted():
CP = SimpleNamespace(carFingerprint=CAR.CHEVROLET_VOLT_CAMERA)
cs = _cs(True, AccState.FAULTED)
@@ -42,7 +42,13 @@ IONIQ_6_RESPONSE_MULTIPLIER = 1.2
IONIQ_6_CANFD_SCC_ACCEL_STEP = (6.0 / 50.0) * IONIQ_6_RESPONSE_MULTIPLIER
IONIQ_6_CANFD_SCC_DECEL_STEP = (15.0 / 50.0) * IONIQ_6_RESPONSE_MULTIPLIER
EV9_CANFD_SCC_DECEL_STEP = 10.0 / 50.0
RAY_PEDAL_COMMAND_CAP = 0.55
RAY_PEDAL_COMMAND_CAP = 0.70
RAY_PEDAL_CAP_SPEED_BP = [0.0, 12.0, 20.0, 25.0]
RAY_PEDAL_CAP_V = [0.55, 0.55, 0.65, RAY_PEDAL_COMMAND_CAP]
RAY_PEDAL_OFFSET_SPEED_BP = [0.0, 2.0, 4.0, 8.0, 12.0, 16.0, 20.0, 25.0]
RAY_PEDAL_OFFSET_V = [0.08, 0.13, 0.20, 0.32, 0.42, 0.51, 0.565, 0.60]
RAY_PEDAL_ACCEL_GAIN = 0.22
RAY_PEDAL_DECEL_GAIN = 0.55
RAY_PEDAL_RATE_UP = 0.02
RAY_PEDAL_RATE_DOWN = 0.06
RAY_PEDAL_OVERSPEED_CUTOFF = 0.5
@@ -91,6 +97,25 @@ BLINDSPOT_WARNING_FLASH_ON_SAMPLES = 16
BLINDSPOT_WARNING_SOUND_SAMPLES = 36
def get_ray_pedal_target(accel: float, v_ego: float, set_speed: float) -> float:
if not all(np.isfinite(x) for x in (accel, v_ego, set_speed)) or set_speed < 1.0:
return 0.0
speed_error = set_speed - v_ego
if speed_error <= -RAY_PEDAL_OVERSPEED_CUTOFF:
return 0.0
pedal_offset = float(np.interp(v_ego, RAY_PEDAL_OFFSET_SPEED_BP, RAY_PEDAL_OFFSET_V))
pedal_cap = float(np.interp(v_ego, RAY_PEDAL_CAP_SPEED_BP, RAY_PEDAL_CAP_V))
pedal_gain = RAY_PEDAL_DECEL_GAIN if accel < 0.0 else RAY_PEDAL_ACCEL_GAIN
target = float(np.clip(pedal_offset + accel * pedal_gain, 0.0, pedal_cap))
if speed_error < 0.0:
target *= float(np.clip(0.65 * (1.0 + speed_error / RAY_PEDAL_OVERSPEED_CUTOFF), 0.0, 1.0))
elif speed_error < RAY_PEDAL_TAPER_BELOW_TARGET:
target *= 0.65 + 0.35 * speed_error / RAY_PEDAL_TAPER_BELOW_TARGET
return target
def egmp_dynamic_longitudinal_tuning(CP) -> bool:
return CP.carFingerprint in (CAR.HYUNDAI_IONIQ_6, CAR.KIA_EV9, CAR.HYUNDAI_IONIQ_5_PE) or \
kia_ev6_gt_line_longitudinal_tuning(
@@ -834,27 +859,12 @@ class CarController(CarControllerBase):
pedal_active = (CC.longActive and pedal_ready and not CC.cruiseControl.override and
not CS.out.gasPressed and not CS.out.brakePressed)
if pedal_active:
set_speed = hud_control.setSpeed
if not np.isfinite(set_speed) or set_speed < 1.0:
target = get_ray_pedal_target(accel, CS.out.vEgo, hud_control.setSpeed)
if target <= 0.001:
self._ray_pedal_gas_last = 0.0
else:
speed_error = set_speed - CS.out.vEgo
if speed_error <= -RAY_PEDAL_OVERSPEED_CUTOFF:
self._ray_pedal_gas_last = 0.0
else:
pedal_offset = float(np.interp(CS.out.vEgo, [0., 2., 4., 8., 12., 20.],
[0.08, 0.13, 0.20, 0.32, 0.42, 0.48]))
pedal_gain = 2.0 if accel < 0.0 else 0.22
target = float(np.clip(pedal_offset + accel * pedal_gain, 0.0, RAY_PEDAL_COMMAND_CAP))
if speed_error < 0.0:
target *= float(np.clip(0.65 * (1.0 + speed_error / RAY_PEDAL_OVERSPEED_CUTOFF), 0.0, 1.0))
elif speed_error < RAY_PEDAL_TAPER_BELOW_TARGET:
target *= 0.65 + 0.35 * speed_error / RAY_PEDAL_TAPER_BELOW_TARGET
if target <= 0.001:
self._ray_pedal_gas_last = 0.0
else:
next_gas = rate_limit(target, self._ray_pedal_gas_last, -RAY_PEDAL_RATE_DOWN, RAY_PEDAL_RATE_UP)
self._ray_pedal_gas_last = min(next_gas, target)
next_gas = rate_limit(target, self._ray_pedal_gas_last, -RAY_PEDAL_RATE_DOWN, RAY_PEDAL_RATE_UP)
self._ray_pedal_gas_last = min(next_gas, target)
else:
self._ray_pedal_gas_last = 0.0
can_sends.append(create_gas_interceptor_command(
@@ -317,6 +317,11 @@ class CarInterface(CarInterfaceBase):
ret.autoResumeSng = False
ret.minEnableSpeed = -1.0
ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.LONG.value
ret.longitudinalTuning.kpBP = [0.0, 5.0, 15.0, 35.0]
ret.longitudinalTuning.kpV = [0.095, 0.085, 0.065, 0.050]
ret.longitudinalTuning.kiBP = [0.0, 3.0, 6.0, 35.0]
ret.longitudinalTuning.kiV = [0.07, 0.10, 0.15, 0.24]
ret.longitudinalTuning.kfDEPRECATED = 1.0
# Car specific configuration overrides
@@ -51,6 +51,11 @@ RADAR_TRACK_CONFIGS = {
def get_radar_track_config(car_fingerprint, flags: int = 0) -> RadarTrackConfig | None:
if (car_fingerprint == CAR.HYUNDAI_PALISADE_2023 and
flags & HyundaiFlags.CAN_CANFD_BLENDED and flags & HyundaiFlags.CANFD_LKA_STEERING):
return RadarTrackConfig(GV70_RADAR_START_ADDR, GV70_RADAR_MSG_COUNT, "palisade_210", bus=0,
frequency=20, expected_length=32, dbc_name=GV70_RADAR_DBC)
if car_fingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN:
return RadarTrackConfig(GV70_RADAR_START_ADDR, GV70_RADAR_MSG_COUNT, "gv70_210", bus=0,
frequency=20, expected_length=32, dbc_name=GV70_RADAR_DBC)
@@ -73,7 +78,7 @@ def radar_tracks_available(radar_config: RadarTrackConfig | None, fingerprint) -
if radar_config is None:
return False
if radar_config.radar_type == "gv70_210":
if radar_config.radar_type in ("gv70_210", "palisade_210"):
return all(fingerprint[radar_config.bus].get(addr) == radar_config.expected_length
for addr in range(radar_config.start_addr, radar_config.start_addr + radar_config.msg_count))
@@ -236,7 +241,7 @@ class RadarInterface(RadarInterfaceBase):
del self.pts[track_key]
continue
if radar_type == "gv70_210":
if radar_type in ("gv70_210", "palisade_210"):
for i in ("1", "2"):
track_key = addr * 2 + int(i) - 1
valid = msg[f"{i}_STATE"] in (3, 4)
@@ -760,6 +760,107 @@ class TestHyundaiFingerprint:
assert palisade_2023.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CAN_CANFD_BLENDED
assert palisade_2023.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CANCEL_BTN_ENABLE
@pytest.mark.parametrize("alpha_long", [False, True])
def test_palisade_hda2_front_radar_is_available_with_complete_native_bank(self, alpha_long):
fingerprint = gen_empty_fingerprint()
fingerprint[2][0x50] = 16
car_fw = [CarParams.CarFw(ecu=Ecu.adas, fwVersion=b"", address=0x730, brand="hyundai")]
no_radar = CarInterface.get_params(CAR.HYUNDAI_PALISADE_2023, fingerprint, car_fw, alpha_long, False, False, None)
assert no_radar.radarUnavailable
config = get_radar_track_config(no_radar.carFingerprint, no_radar.flags)
assert config.radar_type == "palisade_210"
assert config.start_addr == 0x210
assert config.msg_count == 16
assert config.expected_length == 32
assert config.frequency == 20
assert config.bus == CanBus(no_radar).ACAN == 0
for addr in range(0x210, 0x220):
fingerprint[0][addr] = 32
CP = CarInterface.get_params(CAR.HYUNDAI_PALISADE_2023, fingerprint, car_fw, alpha_long, False, False, None)
assert not CP.radarUnavailable
assert CP.openpilotLongitudinalControl == no_radar.openpilotLongitudinalControl
assert CP.flags == no_radar.flags
assert CP.safetyConfigs[-1].safetyParam == no_radar.safetyConfigs[-1].safetyParam
assert DBC[CP.carFingerprint][Bus.pt] == "hyundai_palisade_2023_generated"
@pytest.mark.parametrize("missing", [0x210, 0x218, 0x21f])
def test_palisade_hda2_incomplete_radar_bank_stays_unavailable(self, missing):
fingerprint = gen_empty_fingerprint()
fingerprint[2][0x50] = 16
for addr in range(0x210, 0x220):
if addr != missing:
fingerprint[0][addr] = 32
CP = CarInterface.get_params(CAR.HYUNDAI_PALISADE_2023, fingerprint, [], False, False, False, None)
assert CP.radarUnavailable
@pytest.mark.parametrize("bus,length", [(0, 8), (0, 24), (1, 32), (2, 32), (128, 32), (130, 32)])
def test_palisade_hda2_wrong_radar_layout_or_forwarded_copy_stays_unavailable(self, bus, length):
fingerprint = gen_empty_fingerprint()
fingerprint[2][0x50] = 16
if bus < len(fingerprint):
for addr in range(0x210, 0x220):
fingerprint[bus][addr] = length
CP = CarInterface.get_params(CAR.HYUNDAI_PALISADE_2023, fingerprint, [], False, False, False, None)
assert CP.radarUnavailable
def test_palisade_front_radar_configuration_does_not_enable_non_hda2(self):
fingerprint = gen_empty_fingerprint()
for addr in range(0x210, 0x220):
fingerprint[0][addr] = 32
CP = CarInterface.get_params(CAR.HYUNDAI_PALISADE_2023, fingerprint, [], False, False, False, None)
assert not CP.flags & HyundaiFlags.CANFD_LKA_STEERING
assert get_radar_track_config(CP.carFingerprint, CP.flags) is None
assert CP.radarUnavailable
old_palisade = get_radar_track_config(CAR.HYUNDAI_PALISADE)
assert old_palisade.radar_type == "mando"
assert old_palisade.bus == 1
@pytest.mark.parametrize("data_hex,expected", [
("0da6690a412630ff79e0fcba3f05c0f90000000b2420410671b101d3bf0080fc",
[(6.05, -2.5, -0.7, -1.25, 0.05), (18.45, 1.35, -0.45, -0.7, 0.0)]),
("2cd9d10a381b30fe7c830195be00c0000000000a237830fa95c003f83e0f00fb",
[(44.6, 1.2, -3.63, 0.15, 0.0), (7.45, 3.0, -2.64, -1.0, 0.15)]),
])
def test_palisade_front_radar_decoder_matches_detailed_reference_frame(self, data_hex, expected):
CP = CarParams(carFingerprint=CAR.HYUNDAI_PALISADE_2023,
flags=int(HyundaiFlags.CAN_CANFD_BLENDED | HyundaiFlags.CANFD_LKA_STEERING), radarUnavailable=False)
radar = RadarInterface(CP)
data = bytes.fromhex(data_hex)
radar.rcp.update([(1, [(0x210, data, 0)])])
points = radar._update({0x210}).points
assert len(points) == 2
for point, values in zip(points, expected, strict=True):
assert point.measured
assert (point.dRel, point.yRel, point.vRel, point.aRel, point.yvRel) == pytest.approx(values)
def test_palisade_front_radar_emits_32_tracks_without_duplicate_camera_bus_copy(self):
CP = CarParams(carFingerprint=CAR.HYUNDAI_PALISADE_2023,
flags=int(HyundaiFlags.CAN_CANFD_BLENDED | HyundaiFlags.CANFD_LKA_STEERING), radarUnavailable=False)
radar = RadarInterface(CP)
packer = CANPacker(radar.radar_config.dbc_name)
messages = []
for addr in range(0x210, 0x220):
msg = packer.make_can_msg(f"RADAR_TRACK_{addr:x}", 0, {
"1_STATE": 3, "1_LONG_DIST": 25.0, "1_LAT_DIST": 0.5, "1_REL_SPEED": -2.0,
"2_STATE": 4, "2_LONG_DIST": 35.0, "2_LAT_DIST": -1.0, "2_REL_SPEED": -3.0,
})
data = bytearray(msg[1])
checksum = hkg_can_fd_checksum(addr, None, data)
data[0] = checksum & 0xff
data[1] = checksum >> 8
messages.append((addr, bytes(data), 0))
copies = [(addr, data, 2) for addr, data, _ in messages]
assert radar.update([(1, copies)]) is None
result = radar.update([(2, messages + copies)])
assert result is not None
assert len(result.points) == 32
assert not result.errors.canError
assert len({point.trackId for point in result.points}) == 32
for addr, _ in radar.track_addrs:
radar.rcp.vl[addr]["1_STATE"] = 0
radar.rcp.vl[addr]["2_STATE"] = 0
assert not radar._update(set()).points
def test_palisade_telluride_hda2_uses_mixed_can_layout(self):
fingerprint = gen_empty_fingerprint()
fingerprint[2][0x50] = 16
@@ -1,10 +1,11 @@
from types import SimpleNamespace
import numpy as np
import pytest
from opendbc.can import CANPacker, CANParser
from opendbc.car import Bus, gen_empty_fingerprint
from opendbc.car.hyundai.carcontroller import CarController
from opendbc.car.hyundai.carcontroller import CarController, get_ray_pedal_target, RAY_PEDAL_COMMAND_CAP
from opendbc.car.hyundai.carstate import CarState
from opendbc.car.hyundai.interface import CarInterface
from opendbc.car.hyundai.values import CAR, DBC, HyundaiSafetyFlags
@@ -34,6 +35,9 @@ def test_ray_pedal_fingerprint_isolation(candidate, fingerprint, has_pedal):
assert CP.safetyConfigs[-1].safetyParam == 0x9405
assert CP.minEnableSpeed == -1.0
assert not CP.autoResumeSng
assert all(gain > 0 for gain in CP.longitudinalTuning.kpV)
assert all(gain > 0 for gain in CP.longitudinalTuning.kiV)
assert CP.longitudinalTuning.kfDEPRECATED == 1.0
FPCP = CarInterface.get_starpilot_params(candidate, fingerprint, [], CP, SimpleNamespace())
assert FPCP.canUsePedal
assert not FPCP.pcmCruiseSpeed
@@ -41,6 +45,8 @@ def test_ray_pedal_fingerprint_isolation(candidate, fingerprint, has_pedal):
else:
assert CP.pcmCruise
assert not (CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.LONG)
assert list(CP.longitudinalTuning.kpV) == [0.0]
assert list(CP.longitudinalTuning.kiV) == [0.0]
def test_ray_pedal_safety_signature_is_unique_across_hyundai_platforms():
@@ -91,12 +97,18 @@ def test_ray_pedal_fault_clears_only_with_healthy_sensor_state():
assert not ret.accFaulted
def test_ray_driver_override_uses_physical_interceptor_tracks():
@pytest.mark.parametrize("physical_rest", [
"010801f30cef", # September 15 calibration: raw 264/497.
"009b01ad0ef0", # October 9 route 0000007b segment 6: raw 155/429.
"00b601c4074b", # October 11 route 00000096 segment 28: raw 182/452.
])
def test_ray_driver_override_uses_physical_interceptor_tracks(physical_rest):
CP = CarInterface.get_params(CAR.KIA_RAY_EV, ray_fingerprint(), [], False, False, False, None)
state = CarState(CP, None)
parsers = state.get_can_parsers(CP)
native_gas = (0x371, bytes.fromhex("004e008000ae0700"), 0)
physical_rest = (0x201, bytes.fromhex("010801f30cef"), 0)
rest_data = bytes.fromhex(physical_rest)
physical_rest = (0x201, rest_data, 0)
for parser in parsers.values():
parser.update([(1_000_000_000, [native_gas, physical_rest])])
ret, _ = state.update(parsers, SimpleNamespace())
@@ -107,7 +119,7 @@ def test_ray_driver_override_uses_physical_interceptor_tracks():
physical_press = packer.make_can_msg("GAS_SENSOR", 0, {
"INTERCEPTOR_GAS": (310 - 264) * 0.672,
"INTERCEPTOR_GAS2": (593 - 497) * 0.332,
"STATE": 0, "COUNTER_PEDAL": 13,
"STATE": 0, "COUNTER_PEDAL": ((rest_data[4] & 0xF) + 1) & 0xF,
})
for parser in parsers.values():
parser.update([(1_020_000_000, [physical_press])])
@@ -128,6 +140,92 @@ def test_ray_without_pedal_keeps_native_gas_detection():
assert ret.gasPressed
@pytest.mark.parametrize(("speed", "cap"), [(0.0, 0.55), (12.0, 0.55), (16.0, 0.60), (20.0, 0.65), (25.0, 0.70), (40.0, 0.70)])
def test_ray_highway_headroom_preserves_low_speed_cap(speed, cap):
assert get_ray_pedal_target(3.5, speed, speed + 10.0) == pytest.approx(cap)
high_set_target = get_ray_pedal_target(3.5, speed, 145.0 / 3.6)
assert 0.0 < high_set_target <= cap + 1e-9 # high SET speed must not inhibit actuation
if speed < 39.0:
assert high_set_target == pytest.approx(cap)
@pytest.mark.parametrize("speed", [0.0, 2.0, 8.0, 12.0, 20.0, 25.0])
def test_ray_pedal_decel_mapping_is_continuous_and_monotonic(speed):
coast = get_ray_pedal_target(0.0, speed, speed + 10.0)
mild_decel = get_ray_pedal_target(-0.1, speed, speed + 10.0)
assert 0.0 < mild_decel < coast
assert coast - mild_decel == pytest.approx(0.055)
assert get_ray_pedal_target(-1.5, speed, speed + 10.0) == 0.0
targets = [get_ray_pedal_target(float(a), speed, speed + 10.0) for a in np.linspace(-3.5, 3.5, 701)]
assert all(0.0 <= x <= RAY_PEDAL_COMMAND_CAP for x in targets)
assert all(b >= a for a, b in zip(targets, targets[1:], strict=False))
assert abs(get_ray_pedal_target(-1e-6, speed, speed + 10.0) - coast) < 1e-6
@pytest.mark.parametrize("speed", [0.0, 12.0, 20.0, 25.0, 40.0])
def test_ray_tuned_pedal_keeps_overspeed_taper_and_cutoff(speed):
full = get_ray_pedal_target(3.5, speed, speed + 10.0)
assert get_ray_pedal_target(3.5, speed, max(speed, 1.0)) <= full
if speed >= 1.5:
assert get_ray_pedal_target(3.5, speed, speed) == pytest.approx(full * 0.65)
assert get_ray_pedal_target(3.5, speed, speed - 0.25) == pytest.approx(full * 0.325)
assert get_ray_pedal_target(3.5, speed, speed - 0.5) == 0.0
@pytest.mark.parametrize(("accel", "speed", "set_speed"), [
(float("nan"), 25.0, 40.0), (1.0, float("nan"), 40.0), (1.0, 25.0, float("nan")),
(float("inf"), 25.0, 40.0), (1.0, float("inf"), 40.0), (1.0, 25.0, float("inf")),
(1.0, 25.0, 0.0),
])
def test_ray_tuned_pedal_rejects_invalid_targets(accel, speed, set_speed):
assert get_ray_pedal_target(accel, speed, set_speed) == 0.0
@pytest.mark.parametrize("cutoff", ["brake", "driver", "override", "inactive", "invalid_sensor", "fault", "overspeed"])
def test_ray_highway_output_cuts_immediately_on_override_or_fault(cutoff):
CP = CarInterface.get_params(CAR.KIA_RAY_EV, ray_fingerprint(), [], False, False, False, None)
controller = CarController(DBC[CP.carFingerprint], CP)
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS11", 0), ("CLU11", 0)], 0)
CS = SimpleNamespace(
lkas11=parser.vl["LKAS11"], clu11=parser.vl["CLU11"],
out=SimpleNamespace(vEgo=25.0, gasPressed=False, brakePressed=False, cruiseState=SimpleNamespace(enabled=False)),
ray_pedal_valid=True, ray_pedal_state=0, is_metric=True,
)
CC = SimpleNamespace(enabled=True, longActive=True, latActive=True,
cruiseControl=SimpleNamespace(cancel=False, resume=False, override=False))
hud = SimpleNamespace(visualAlert=CarControl.HUDControl.VisualAlert.none, setSpeed=40.0,
leftLaneVisible=True, rightLaneVisible=True, leftLaneDepart=False, rightLaneDepart=False)
actuators = SimpleNamespace(longControlState=CarControl.Actuators.LongControlState.pid)
def pedal_msg(frame):
controller.frame = frame
messages = controller.create_can_msgs(True, 0, False, 0.0, 1.5, False, hud, actuators, CS, CC, 2, 0)
return next(dat for addr, dat, bus in messages if addr == 0x200 and bus == 0)
for frame in range(0, 4 * 40, 4):
pedal_msg(frame)
assert controller._ray_pedal_gas_last == pytest.approx(RAY_PEDAL_COMMAND_CAP)
assert pedal_msg(160)[4] & 0x80
if cutoff == "brake":
CS.out.brakePressed = True
elif cutoff == "driver":
CS.out.gasPressed = True
elif cutoff == "override":
CC.cruiseControl.override = True
elif cutoff == "inactive":
CC.longActive = False
elif cutoff == "invalid_sensor":
CS.ray_pedal_valid = False
elif cutoff == "fault":
CS.ray_pedal_state = 5
else:
hud.setSpeed = CS.out.vEgo - 0.5
dat = pedal_msg(164)
assert dat[:4] == bytes(4)
assert not (dat[4] & 0x80)
assert controller._ray_pedal_gas_last == 0.0
@pytest.mark.parametrize("speed", [0.0, 0.1, 1.0, 4.9, 5.0, 12.0])
def test_ray_controller_heartbeats_and_only_actuates_when_ready(speed):
CP = CarInterface.get_params(CAR.KIA_RAY_EV, ray_fingerprint(), [], False, False, False, None)
@@ -227,11 +325,11 @@ def test_ray_controller_heartbeats_and_only_actuates_when_ready(speed):
hud.setSpeed = float('nan')
assert pedal_msg(1.5, 460)[:4] == bytes(4)
hud.setSpeed = 20.0
assert pedal_msg(-0.3, 464)[:4] == bytes(4)
assert pedal_msg(-0.3, 464)[4] & 0x80 # partial pedal trims regen for mild slowing
CS.out.vEgo = 15.0
hud.setSpeed = 53.0 / 3.6
pedal_msg(-0.16, 468)
assert controller._ray_pedal_gas_last < 0.1
assert controller._ray_pedal_gas_last < get_ray_pedal_target(-0.16, 15.0, 20.0)
CS.out.vEgo = 12.0
hud.setSpeed = 8.0 / 3.6
assert pedal_msg(-0.3, 472)[:4] == bytes(4)
@@ -240,7 +338,7 @@ def test_ray_controller_heartbeats_and_only_actuates_when_ready(speed):
assert controller._ray_pedal_gas_last == pytest.approx(0.02)
assert pedal_msg(1.5, 480)[4] & 0x80
assert controller._ray_pedal_gas_last == pytest.approx(0.04)
assert pedal_msg(-0.3, 484)[:4] == bytes(4)
assert pedal_msg(-0.3, 484)[4] & 0x80
@pytest.mark.parametrize("candidate", [CAR.KIA_RAY_EV, CAR.HYUNDAI_KONA_EV_NON_SCC])
@@ -1229,6 +1229,7 @@ CANFD_RADAR_ECU_KEEPALIVE_CAR = {
CAR.GENESIS_GV60_EV_1ST_GEN,
}
RADAR_LIVE_LONGITUDINAL_CAR = CANFD_RADAR_LIVE_LONGITUDINAL_CAR | {
CAR.HYUNDAI_PALISADE_2023,
CAR.HYUNDAI_IONIQ,
CAR.HYUNDAI_KONA_EV_2022,
CAR.HYUNDAI_SANTA_FE_2022,
@@ -49,6 +49,7 @@ class CarController(CarControllerBase):
self.ascent_angle_initialized = False
self.ascent_aol_arm_frames = 0
self.ascent_es_distance_counter_last = None
self.ascent_lkas_alert_counter_last = None
self.cruise_button_prev = 0
self.steer_rate_counter = 0
@@ -301,6 +302,16 @@ class CarController(CarControllerBase):
return self.angle_lkas_active
return CC.latActive
def _ascent_lkas_alert(self, CC, CS):
if self.CP.carFingerprint != CAR.SUBARU_ASCENT_2023 or not self.CP.flags & SubaruFlags.ASCENT_LKAS_ALERT:
return None
dat = getattr(CS, "es_lkas_alert_dat", b"")
msg = subarucan.create_ascent_lkas_alert(dat, self._lkas_status_active(CC), CC.hudControl.visualAlert)
if msg is None or (dat[1] & 0xf) == self.ascent_lkas_alert_counter_last:
return None
self.ascent_lkas_alert_counter_last = dat[1] & 0xf
return msg
def update(self, CC, CS, now_nanos, starpilot_toggles):
actuators = CC.actuators
hud_control = CC.hudControl
@@ -352,6 +363,10 @@ class CarController(CarControllerBase):
cruise_brake = CarControllerParams.BRAKE_MIN
# *** alerts and pcm cancel ***
lkas_alert_msg = self._ascent_lkas_alert(CC, CS)
if lkas_alert_msg is not None:
can_sends.append(lkas_alert_msg)
if self.CP.flags & SubaruFlags.PREGLOBAL:
if self.frame % 5 == 0:
# 1 = main, 2 = set shallow, 3 = set deep, 4 = resume shallow, 5 = resume deep
+5 -1
View File
@@ -29,6 +29,7 @@ class CarState(CarStateBase):
self.stop_start_state = 0
self.avh_frames = {}
self.cruise_buttons_msg = {}
self.es_lkas_alert_dat = b""
self.cruise_buttons = {button: 0 for button in SUBARU_CRUISE_BUTTONS}
def update(self, can_parsers, starpilot_toggles) -> structs.CarState:
@@ -166,6 +167,8 @@ class CarState(CarStateBase):
self.es_distance_msg = copy.copy(cp_es_distance.vl["ES_Distance"])
self.es_dashstatus_msg = copy.copy(cp_cam.vl["ES_DashStatus"])
if self.CP.flags & SubaruFlags.ASCENT_LKAS_ALERT:
self.es_lkas_alert_dat = cp_cam.vl_raw[0x3C4]
if self.CP.flags & SubaruFlags.SEND_INFOTAINMENT:
self.es_infotainment_msg = copy.copy(cp_cam.vl["ES_Infotainment"])
@@ -183,9 +186,10 @@ class CarState(CarStateBase):
@staticmethod
def get_can_parsers(CP):
avh_messages = [(a, 0) for a in (0x6BB, 0x32B, 0x40, 0x48)] if CP.carFingerprint == CAR.SUBARU_LEGACY_2025 else []
camera_messages = [(0x3C4, 0)] if CP.flags & SubaruFlags.ASCENT_LKAS_ALERT else []
parsers = {
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus.main_for_cp(CP)),
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus.camera),
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], camera_messages, CanBus.camera),
Bus.alt: CANParser(DBC[CP.carFingerprint][Bus.pt], avh_messages, CanBus.alt_for_cp(CP))
}
if CP.flags & SubaruFlags.D_PLATFORM:
@@ -25,6 +25,9 @@ class CarInterface(CarInterfaceBase):
if not (ret.flags & SubaruFlags.PREGLOBAL) and 0x323 in fingerprint[2]:
ret.flags |= SubaruFlags.SEND_INFOTAINMENT.value
if candidate == CAR.SUBARU_ASCENT_2023 and fingerprint[CanBus.camera].get(0x3C4) == 8:
ret.flags |= SubaruFlags.ASCENT_LKAS_ALERT.value
if ret.flags & SubaruFlags.PREGLOBAL:
ret.enableBsm = 0x25c in fingerprint[0]
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.subaruPreglobal)]
@@ -40,6 +43,8 @@ class CarInterface(CarInterfaceBase):
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.D_PLATFORM.value
if ret.flags & SubaruFlags.D_PLATFORM_CAMERA:
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.D_PLATFORM_CAMERA.value
if ret.flags & SubaruFlags.ASCENT_LKAS_ALERT:
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.ASCENT_LKAS_ALERT.value
if candidate in SUBARU_STOP_START_CARS:
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.STOP_START_BUTTON.value
if candidate == CAR.SUBARU_LEGACY_2025:
@@ -301,6 +301,18 @@ def create_es_infotainment(packer, frame, es_infotainment_msg, visual_alert, bus
return packer.make_can_msg("ES_Infotainment", bus, values)
def create_ascent_lkas_alert(raw_dat: bytes, lateral_active: bool, visual_alert):
address = 0x3C4
if len(raw_dat) != 8 or raw_dat[0] != (((address & 0xff) + (address >> 8) + sum(raw_dat[1:])) & 0xff):
return None
dat = bytearray(raw_dat)
if lateral_active and visual_alert != VisualAlert.steerRequired and dat[2:] == b"\x00\x01\x04\x00\x00\x00":
dat[3:5] = b"\x00\x00"
dat[0] = ((address & 0xff) + (address >> 8) + sum(dat[1:])) & 0xff
return address, bytes(dat), CanBus.main
def create_es_highbeamassist(packer):
values = {
"HBA_Available": False,
@@ -879,6 +879,88 @@ def test_ascent_hud_waits_for_angle_request():
assert controller._lkas_status_active(CC)
@pytest.mark.parametrize("platform", list(CAR))
def test_secondary_lkas_alert_is_detected_only_on_ascent(platform):
fingerprint = gen_empty_fingerprint()
fingerprint[CanBus.camera][0x3C4] = 8
CP = CarInterface.get_params(platform, fingerprint, [], False, False, False, None)
expected = platform == CAR.SUBARU_ASCENT_2023
assert bool(CP.flags & SubaruFlags.ASCENT_LKAS_ALERT) == expected
assert bool(CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.ASCENT_LKAS_ALERT) == expected
assert (0x3C4 in CarState.get_can_parsers(CP)[Bus.cam].addresses) == expected
@pytest.mark.parametrize("bus,length", [(0, 8), (1, 8), (2, 4), (2, 16)])
def test_ascent_secondary_alert_requires_native_camera_frame(bus, length):
fingerprint = gen_empty_fingerprint()
fingerprint[bus][0x3C4] = length
CP = CarInterface.get_params(CAR.SUBARU_ASCENT_2023, fingerprint, [], False, False, False, None)
assert not CP.flags & SubaruFlags.ASCENT_LKAS_ALERT
assert not CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.ASCENT_LKAS_ALERT
@pytest.mark.parametrize("active,visual_alert,masked", [
(True, structs.CarControl.HUDControl.VisualAlert.none, True),
(False, structs.CarControl.HUDControl.VisualAlert.none, False),
(True, structs.CarControl.HUDControl.VisualAlert.steerRequired, False),
(True, structs.CarControl.HUDControl.VisualAlert.fcw, True),
])
def test_ascent_secondary_hands_on_payload_is_filtered_only_during_active_lateral(active, visual_alert, masked):
raw = bytes.fromhex("ce02000104000000")
address, dat, bus = subarucan.create_ascent_lkas_alert(raw, active, visual_alert)
assert address == 0x3C4
assert bus == CanBus.main
assert dat[1] == raw[1]
assert dat[0] == ((address & 0xff) + (address >> 8) + sum(dat[1:])) & 0xff
assert dat[2:] == (bytes(6) if masked else raw[2:])
@pytest.mark.parametrize("payload", [
"000000000000", "000000110000", "000000100000", "1e0030000000",
"000704000000", "000000010000", "1b0002000000", "000002000000",
"0e0000000000", "190000100000", "1c0704000000", "190000110000",
"190000000000", "190000010000", "010104000000", "000604000000",
])
def test_ascent_secondary_alert_preserves_other_alerts_and_opaque_data(payload):
raw = bytearray(bytes.fromhex("0000" + payload))
raw[0] = (0xc4 + 3 + sum(raw[1:])) & 0xff
assert subarucan.create_ascent_lkas_alert(bytes(raw), True, structs.CarControl.HUDControl.VisualAlert.none)[1] == raw
@pytest.mark.parametrize("raw", [b"", bytes(7), bytes(9), bytes.fromhex("0002000104000000")])
def test_ascent_secondary_alert_does_not_transmit_invalid_data(raw):
assert subarucan.create_ascent_lkas_alert(raw, True, structs.CarControl.HUDControl.VisualAlert.none) is None
def test_ascent_secondary_alert_uses_fresh_camera_counter_and_actual_angle_request():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
CP.flags |= SubaruFlags.ASCENT_LKAS_ALERT.value
controller = CarController({}, CP)
CC = SimpleNamespace(latActive=True, hudControl=SimpleNamespace(visualAlert=structs.CarControl.HUDControl.VisualAlert.none))
CS = SimpleNamespace(es_lkas_alert_dat=bytes.fromhex("ce02000104000000"))
assert controller._ascent_lkas_alert(CC, CS)[1] == CS.es_lkas_alert_dat
assert controller._ascent_lkas_alert(CC, CS) is None
controller.angle_lkas_active = True
for counter in (3, 15, 0):
dat = bytearray(CS.es_lkas_alert_dat)
dat[1] = counter
dat[0] = (0xc4 + 3 + sum(dat[1:])) & 0xff
CS.es_lkas_alert_dat = bytes(dat)
msg = controller._ascent_lkas_alert(CC, CS)
assert msg[1][1] == counter
assert msg[1][2:] == bytes(6)
assert controller._ascent_lkas_alert(CC, CS) is None
@pytest.mark.parametrize("platform", [CAR.SUBARU_ASCENT, CAR.SUBARU_OUTBACK_2023, CAR.SUBARU_LEGACY_2025, CAR.SUBARU_CROSSTREK_2025])
def test_secondary_alert_controller_does_not_touch_other_subarus(platform):
CP = CarInterface.get_non_essential_params(platform)
CP.flags |= SubaruFlags.ASCENT_LKAS_ALERT.value
controller = CarController({}, CP)
assert controller._ascent_lkas_alert(None, None) is None
@pytest.mark.parametrize("platform", [CAR.SUBARU_ASCENT_2023, CAR.SUBARU_OUTBACK_2023, CAR.SUBARU_LEGACY_2025,
CAR.SUBARU_CROSSTREK_2025, CAR.SUBARU_ASCENT])
def test_stock_cruise_cancel_fresh_frame_gate_is_ascent_angle_only(platform):
@@ -91,6 +91,7 @@ class SubaruSafetyFlags(IntFlag):
STOP_START_BUTTON = 256
REDNECK_CRUISE = 512
AVH_STARTUP = 1024
ASCENT_LKAS_ALERT = 2048
LEGACY_2025_ANGLE_LIMITS = FIXED_ANGLE_LIMITS
@@ -110,6 +111,7 @@ class SubaruFlags(IntFlag):
LKAS_ANGLE = 64
D_PLATFORM = 128
D_PLATFORM_CAMERA = 256
ASCENT_LKAS_ALERT = 512
GLOBAL_ES_ADDR = 0x787
@@ -0,0 +1,59 @@
"""Continental ARS510 ACC-target radar, one point per radar cycle.
Adapted from ds-sebastian/ars510-radar at fb5b29c2b5999f34afeb6f7ac494a73949d66c67.
Copyright (c) 2026 ARS510 radar contributors. MIT license; see THIRD_PARTY_NOTICES.md.
"""
from math import nan
from opendbc.can import CANParser
from opendbc.car.structs import RadarData
from opendbc.car.interfaces import RadarInterfaceBase
RADAR_DBC = "toyota_tss2_ars510"
TARGET_ADDRS = {0x235, 0x237, 0x239}
class RadarInterface(RadarInterfaceBase):
def __init__(self, CP):
super().__init__(CP)
self.rcp = CANParser(RADAR_DBC, [("ACC_TARGET_1", 50), ("ACC_TARGET_2", 50), ("ACC_TARGET_3", 50)], 1)
self.trigger_msg = 0x237
self.updated_messages = set()
self.cycle = None
self.last_record_t = None
def update(self, can_strings):
self.frame += 1
packets = [(nanos, [(addr, dat, src) for addr, dat, src in frames if addr not in TARGET_ADDRS or len(dat) == 8])
for nanos, frames in can_strings]
self.updated_messages.update(self.rcp.update(packets))
t = can_strings[-1][0] * 1e-9 if can_strings else None
cycle = self.rcp.vl["ACC_TARGET_3"]["TIMESTAMP"]
clock_seen = bool(self.rcp.ts_nanos["ACC_TARGET_3"]["TIMESTAMP"])
if self.trigger_msg not in self.updated_messages or not clock_seen or cycle == self.cycle:
# Keep card publishing through startup or a stopped cycle clock, without erasing healthy tracks between scans.
if t is None or self.frame % 5 != 0 or (self.last_record_t is not None and t - self.last_record_t <= 0.5):
return None
ret = RadarData()
ret.errors.radarUnavailableTemporary = self.last_record_t is not None
return ret
self.cycle = cycle
self.updated_messages.clear()
self.last_record_t = t
ret = RadarData()
if not self.rcp.can_valid:
ret.errors.canError = True
return ret
target, position = self.rcp.vl["ACC_TARGET_1"], self.rcp.vl["ACC_TARGET_2"]
if target["STATE"] == 5 and position["PRESENT"]:
pt = RadarData.RadarPoint()
pt.trackId = int(target["TARGET_ID"])
pt.dRel = position["LONG_DIST"]
pt.yRel = position["LAT_DIST"]
pt.vRel = target["REL_SPEED"]
pt.aRel = pt.yvRel = nan
pt.measured = True
ret.points = [pt]
return ret
@@ -0,0 +1,230 @@
"""Toyota / Continental ARS510 front radar (RAV4 2022+, fwdRadar 8821F0R03100): object list to RadarData.
The radar sends its object list on bus 1 as one 742-byte record split over 106 frames on 0x80 (consecutive-frame
numbers wrap every 16 frames, so it is reassembled here instead of read through a DBC). Each of its 20 slots holds one
object; the radar keeps an object in its slot while the slot's age counts up, which gives the track IDs.
The slot speed has slow, correlated errors at range (false closings of 1-10 s beyond ~40 m). Their size follows the
radar's 240|7 code. One Kalman filter per track fuses the slot speed, weighted by that code, with the radar's own ACC target
(0x235 / 0x237) for the track it describes. radard then filters as for any radar. radard's lead match weights
lateral position only through the camera's uncertainty, so beyond 15 m a track a lane away from the yaw-predicted path
is withheld.
Adapted from ds-sebastian/ars510-radar at fb5b29c2b5999f34afeb6f7ac494a73949d66c67.
Copyright (c) 2026 ARS510 radar contributors. MIT license; see THIRD_PARTY_NOTICES.md.
"""
import zlib
from math import isfinite, nan, pi
from opendbc.car import structs
from opendbc.car.interfaces import RadarInterfaceBase
RADAR_BUS, CAR_BUS = 1, 0
SPEED_ADDR, YAW_ADDR, OBJECTS_ADDR, ACC_SPEED_ADDR, ACC_POS_ADDR = 0xB4, 0x24, 0x80, 0x235, 0x237
RECORD_FRAMES, RECORD_LEN, SLOT_START, SLOT_LEN, SLOTS = 106, 742, 17, 36, 20
IDLE_SLOT = bytes.fromhex("FCE00000A0F07F00FFFDF71FFFA100F807000F0008000000000000000000000000000000")
EGO_MAX_AGE_S = 0.5
PUBLISH_AGE = 60 # ~3.6 s: young tracks have unconverged range and speed
RANGE_GAIN = 0.1 # velocity-aided range: cuts 1.5 s range walks by about a third
# Kalman speed filter (one state: the lead's over-ground speed)
SIGMA_PER_CODE = 0.045 # m/s per 240|7 count, against the radar's ACC target
YOUNG_SCALE, YOUNG_AGE = 1.8, 100 # young tracks err more than 240|7 says
ACC_SIGMA = 0.13 # m/s, the radar's ACC target speed (its measured noise; blended with the object list, which damps the ACC target's rare slides)
LEAD_ACCEL = 1.5 # m/s^2, process noise
GATE_SIGMA = 3.0 # innovations beyond 3 sigma count as 3 sigma
PUBLISH_STD = 0.75 # m/s: a new track is published once its speed is this certain
PATH_GATE, PATH_GATE_MIN_RANGE = 2.5, 15.0 # m: off the yaw-predicted path by more than this = another lane; closer in, cut-ins stay
FAR_ACC_ONLY = 60.0 # m: beyond this only the car the radar's ACC follows is published, as Toyota's own ACC uses it
STILL, STILL_GATE = 0.5, 1.5 # m/s, m: a stationary track (parked car, roadside object) is path-gated at every range, so it stays out of the lane
def bits(b: bytes, start: int, length: int) -> int:
return (int.from_bytes(b, "little") >> start) & ((1 << length) - 1)
def be_bits(b: bytes, start: int, length: int) -> int:
return (int.from_bytes(b, "big") >> start) & ((1 << length) - 1)
class Ars510Radar:
"""Raw CAN frames in, points (trackId, dRel, yRel, vRel) out once per 0x80 record."""
def __init__(self):
self.chunks, self.record_t, self.last_frame_t = [], 0.0, None
self.v_ego = self.yaw = None # (time, m/s), (time, rad/s left positive)
self.slots = {} # slot -> (radar track id, age, time)
self.next_tid = 1
self.acc_speed = self.acc_pos = self.acc_assoc = None
self.kf = {} # tid -> (time, speed, variance)
self.rng = {} # tid -> (time, dRel, vRel)
self.published = set()
def update(self, t: float, bus: int, addr: int, dat: bytes):
if bus == CAR_BUS and addr == SPEED_ADDR and len(dat) >= 7:
self.v_ego = (t, int.from_bytes(dat[5:7], "big") * 0.01 / 3.6)
elif bus == CAR_BUS and addr == YAW_ADDR and len(dat) >= 2:
self.yaw = (t, (((dat[0] & 0x03) << 8 | dat[1]) * 0.244 - 125.0) * pi / 180.0)
elif bus == RADAR_BUS and addr in (ACC_SPEED_ADDR, ACC_POS_ADDR):
if len(dat) != 8 or (not dat[1] & 4 if addr == ACC_SPEED_ADDR else dat[2:] == bytes.fromhex("003E80000000")):
self.acc_speed = self.acc_pos = None # idle
elif addr == ACC_SPEED_ADDR:
self.acc_speed = (t, (be_bits(dat, 29, 11) - 1024) * 0.125)
else:
self.acc_pos = (t, be_bits(dat, 39, 13) * 0.025, (be_bits(dat, 27, 12) - 2000) * 0.01)
elif bus == RADAR_BUS and addr == OBJECTS_ADDR:
record = self.reassemble(t, dat)
if record is not None and int.from_bytes(record[737:741], "little") == zlib.crc32(record[1:737]):
return self.points(self.record_t, record)
return None
def reassemble(self, t, dat):
"""The 106 frames of one record joined (bytes 1-7 of each); restarts on a first frame (0x12 0xE4)."""
if len(dat) != 8 or (self.chunks and t < self.last_frame_t):
self.chunks = []
if len(dat) != 8:
return None
if dat[0] == 0x12 and dat[1] == 0xE4:
self.chunks, self.record_t = [bytes(dat[1:8])], t
elif self.chunks:
self.chunks.append(bytes(dat[1:8]))
else:
return None
self.last_frame_t = t
if len(self.chunks) != RECORD_FRAMES:
return None
record, self.chunks = b"".join(self.chunks), []
return record if len(record) == RECORD_LEN else None
def track_id(self, t, slot, age):
prev = self.slots.get(slot)
if prev is not None and t == prev[2]:
return prev[0]
if prev is None or t - prev[2] > 0.3 or not (age > prev[1] or age == prev[1] == 126):
prev = (self.next_tid, age, t)
self.next_tid += 1
self.slots[slot] = (prev[0], age, t)
return prev[0]
def points(self, t, record):
v_ego = self.v_ego[1] if self.v_ego is not None and abs(t - self.v_ego[0]) <= EGO_MAX_AGE_S else None
objs = []
for slot in range(SLOTS):
s = record[SLOT_START + SLOT_LEN * slot:SLOT_START + SLOT_LEN * (slot + 1)]
if s == IDLE_SLOT:
continue
age, lat = bits(s, 24, 7), bits(s, 44, 12) - 2048
init_template = age == 1 and bits(s, 56, 7) == 0 and bits(s, 216, 6) == 0
# over-ground speed: 0.149 m/s per code against 0xB4 ego speed (DBC factor 0.15)
objs.append(dict(tid=self.track_id(t, slot, age), age=age, d=(bits(s, 32, 12) - 160) / 16, y=lat * 0.015,
vg=(bits(s, 64, 10) - 510.5) * 0.149, unc=bits(s, 240, 7),
valid=age >= 1 and not init_template and abs(lat) < 2000))
tracks = {o["tid"]: o for o in objs if o["valid"]}
acc_tid = self.acc_match(t, tracks)
yaw = self.yaw[1] if self.yaw is not None and abs(t - self.yaw[0]) <= EGO_MAX_AGE_S else None
out = []
for o in objs:
if not o["valid"]:
continue
tid, vrel, std = o["tid"], nan, 0.0
if v_ego is not None:
readings = [(self.acc_speed[1] + v_ego, ACC_SIGMA)] if tid == acc_tid else []
speed, std = self.speed_filter(tid, t, o, readings)
vrel = speed - v_ego
d_rel = self.fused_range(tid, t, self.acc_pos[1] if tid == acc_tid else o["d"], vrel) # ACC distance for its track
if o["age"] < PUBLISH_AGE or (tid not in self.published and std > PUBLISH_STD) or not isfinite(vrel):
continue # a point needs a fresh ego speed: one NaN would poison radard's filter
still = abs(speed) < STILL and d_rel <= PATH_GATE_MIN_RANGE
if tid != acc_tid and (d_rel > FAR_ACC_ONLY or (yaw is not None and (d_rel > PATH_GATE_MIN_RANGE or still) and abs(
o["y"] - yaw / max(v_ego, 2.0 if still else 5.0) * d_rel * d_rel / 2) > (STILL_GATE if still else PATH_GATE))):
continue # far: only the car the radar's ACC follows is published (the camera covers the rest); closer in: an object off the path is another lane's
self.published.add(tid)
out.append((tid, d_rel, o["y"], vrel))
self.prune(t)
return out
def speed_filter(self, tid, t, o, readings):
"""One-state Kalman filter on the over-ground speed; every reading weighted by its own sigma."""
sig = SIGMA_PER_CODE * max(o["unc"], 1)
if o["age"] < YOUNG_AGE:
sig *= 1.0 + (YOUNG_SCALE - 1.0) * min(max((YOUNG_AGE - o["age"]) / (YOUNG_AGE - PUBLISH_AGE), 0.0), 1.0)
prev = self.kf.get(tid)
if prev is None or not 0.0 < t - prev[0] <= 0.5:
self.kf[tid] = (t, o["vg"], sig * sig)
return o["vg"], sig
_, v, p = prev
p += (LEAD_ACCEL * (t - prev[0])) ** 2
for z, r in [(o["vg"], sig)] + readings:
s = p + r * r
innov = max(min(z - v, GATE_SIGMA * s ** 0.5), -GATE_SIGMA * s ** 0.5)
v, p = v + p / s * innov, p * (1.0 - p / s)
self.kf[tid] = (t, v, p)
return v, p ** 0.5
def fused_range(self, tid, t, d, vrel):
prev = self.rng.get(tid)
if prev is not None and isfinite(vrel) and isfinite(prev[2]) and 0.0 < t - prev[0] <= 0.5:
pred = prev[1] + 0.5 * (prev[2] + vrel) * (t - prev[0])
d = pred + RANGE_GAIN * (d - pred)
self.rng[tid] = (t, d, vrel)
return d
def acc_match(self, t, tracks):
"""The track the radar's ACC target describes: matched by position, kept while both persist."""
if self.acc_speed is None or self.acc_pos is None or max(abs(t - self.acc_speed[0]), abs(t - self.acc_pos[0])) > 0.1:
self.acc_assoc = None
return None
_, ax, ay = self.acc_pos
rs = max(12.0, 0.4 * ax) # the object list reads far cars short of the ACC distance; 10-20 % of new tracks by more than 10 m
costs = sorted((abs(o["d"] - ax) / rs + abs(o["y"] - ay) / 0.5, tid, o["age"]) for tid, o in tracks.items())
if self.acc_assoc is not None:
tid0, ax0, ay0 = self.acc_assoc
own = next((c for c, tid, _ in costs if tid == tid0), None)
if own is not None and own < 4.0 and abs(ax - ax0) <= 8.0 and abs(ay - ay0) <= 1.0:
self.acc_assoc = (tid0, ax, ay)
return tid0
self.acc_assoc = None
if not costs or costs[0][0] >= 1.0 or costs[0][2] < 20 or (len(costs) > 1 and costs[1][0] - costs[0][0] <= 1.0):
return None
self.acc_assoc = (costs[0][1], ax, ay)
return costs[0][1]
def prune(self, t):
for store in (self.kf, self.rng):
if len(store) > 400:
for k in [k for k, v in store.items() if v[0] < t - 5.0]:
store.pop(k)
if len(self.published) > 400:
# A live allocation can outlast its recent speed-filter entry after invalid geometry.
self.published &= set(self.kf) | {state[0] for state in self.slots.values()}
class RadarInterface(RadarInterfaceBase):
def __init__(self, CP):
super().__init__(CP)
self.radar = Ars510Radar()
self.last_record_t = None
def update(self, can_packets):
self.frame += 1
points, t = None, None
for nanos, frames in can_packets:
t = nanos * 1e-9
for address, dat, src in frames:
out = self.radar.update(t, src, address, dat)
if out is not None:
points, self.last_record_t = out, t
if points is None: # between scans: report only at 20 Hz, and only before the first scan or once the radar is stale
if t is None or self.frame % 5 != 0 or (self.last_record_t is not None and t - self.last_record_t <= 0.5):
return None
ret = structs.RadarData()
ret.errors.radarUnavailableTemporary = self.last_record_t is not None
return ret
ret = structs.RadarData()
pts = []
for track_id, d_rel, y_rel, v_rel in points:
pt = structs.RadarData.RadarPoint()
pt.trackId, pt.dRel, pt.yRel, pt.vRel = track_id, d_rel, y_rel, v_rel
pt.aRel = pt.yvRel = nan
pt.measured = True
pts.append(pt)
ret.points = pts
return ret
+12 -1
View File
@@ -5,7 +5,7 @@ from opendbc.car.toyota.radar_interface import RadarInterface
from opendbc.car.toyota.values import Ecu, CAR, DBC, ToyotaFlags, CarControllerParams, TSS2_CAR, RADAR_ACC_CAR, NO_DSU_CAR, \
MIN_ACC_SPEED, EPS_SCALE, NO_STOP_TIMER_CAR, ANGLE_CONTROL_CAR, \
ToyotaSafetyFlags, LEGACY_PRIUS_CAR, TOYOTA_AUTO_HOLD_CARS, \
uses_toyota_auto_hold_aeb
uses_toyota_auto_hold_aeb, ARS510_RADAR_CAR
from opendbc.car.disable_ecu import disable_ecu
from opendbc.car.interfaces import CarInterfaceBase
from opendbc.safety import ALTERNATIVE_EXPERIENCE
@@ -19,6 +19,17 @@ class CarInterface(CarInterfaceBase):
CarController = CarController
RadarInterface = RadarInterface
@classmethod
def get_params(cls, candidate, fingerprint, car_fw, alpha_long, is_release, docs, starpilot_toggles):
ret = super().get_params(candidate, fingerprint, car_fw, alpha_long, is_release, docs, starpilot_toggles)
radar_mode = getattr(starpilot_toggles, "toyota_radar_mode_requested", 0)
if candidate in ARS510_RADAR_CAR and radar_mode in (1, 2):
ret.flags |= ToyotaFlags.ARS510_RADAR.value
if radar_mode == 2:
ret.flags |= ToyotaFlags.ARS510_FUSED.value
ret.radarUnavailable = False
return ret
@staticmethod
def get_pid_accel_limits(CP, current_speed, cruise_speed):
return CarControllerParams(CP).ACCEL_MIN, CarControllerParams(CP).ACCEL_MAX
@@ -2,7 +2,9 @@
from opendbc.can import CANParser
from opendbc.car import Bus
from opendbc.car.structs import RadarData
from opendbc.car.toyota.values import CAR, DBC, TSS2_CAR
from opendbc.car.toyota.values import ARS510_RADAR_CAR, CAR, DBC, TSS2_CAR, ToyotaFlags
from opendbc.car.toyota.ars510_acc_target import RadarInterface as Ars510AccRadarInterface
from opendbc.car.toyota.ars510_radar import RadarInterface as Ars510FusedRadarInterface
from opendbc.car.interfaces import RadarInterfaceBase
RADAR_ACC_TSSP_CAR = {CAR.TOYOTA_CAMRY}
@@ -39,6 +41,12 @@ class RadarInterface(RadarInterfaceBase):
super().__init__(CP)
self.track_id = 0
self.radar_acc_tssp = CP.carFingerprint in RADAR_ACC_TSSP_CAR
self.ars510 = None
if CP.carFingerprint in ARS510_RADAR_CAR and CP.flags & ToyotaFlags.ARS510_RADAR and not CP.radarUnavailable:
parser = Ars510FusedRadarInterface if CP.flags & ToyotaFlags.ARS510_FUSED else Ars510AccRadarInterface
self.ars510 = parser(CP)
self.rcp = self.pt_cp = None
return
if self.radar_acc_tssp:
self.RADAR_MSGS = TSSP_CLUSTER_MSGS
@@ -60,6 +68,8 @@ class RadarInterface(RadarInterfaceBase):
self.updated_messages = set()
def update(self, can_strings):
if self.ars510 is not None:
return self.ars510.update(can_strings)
if self.rcp is None:
return super().update(None)
@@ -0,0 +1,237 @@
from math import isfinite, isnan
from types import SimpleNamespace
import zlib
import pytest
from opendbc.can import CANPacker
from opendbc.car import gen_empty_fingerprint, structs
from opendbc.car.toyota.ars510_acc_target import RadarInterface as AccRadarInterface, RADAR_DBC
from opendbc.car.toyota.ars510_radar import Ars510Radar, IDLE_SLOT, RadarInterface as FusedRadarInterface
from opendbc.car.toyota.interface import CarInterface
from opendbc.car.toyota.radar_interface import RadarInterface
from opendbc.car.toyota.values import ARS510_RADAR_CAR, CAR, ToyotaFlags
@pytest.mark.parametrize("candidate", list(CAR))
@pytest.mark.parametrize("mode", [0, 1, 2, 3])
@pytest.mark.parametrize("alpha_long,can_filter", [(False, False), (True, False), (True, True)])
def test_radar_selection_is_opt_in_and_changes_only_compatible_rav4_radar_fields(candidate, mode, alpha_long, can_filter):
fingerprint = gen_empty_fingerprint()
if can_filter:
fingerprint[0][0x2aa] = 8
baseline = CarInterface.get_params(candidate, fingerprint, [], alpha_long, False, False, None)
selected = CarInterface.get_params(candidate, fingerprint, [], alpha_long, False, False,
SimpleNamespace(toyota_radar_mode_requested=mode))
expected = baseline.to_dict()
if candidate in ARS510_RADAR_CAR and mode in (1, 2):
expected["flags"] |= ToyotaFlags.ARS510_RADAR
if mode == 2:
expected["flags"] |= ToyotaFlags.ARS510_FUSED
expected["radarUnavailable"] = False
radar = RadarInterface(selected)
assert isinstance(radar.ars510, FusedRadarInterface if mode == 2 else AccRadarInterface)
assert radar.rcp is radar.pt_cp is None
assert selected.to_dict() == expected
assert selected.safetyConfigs[0].to_dict() == baseline.safetyConfigs[0].to_dict()
@pytest.mark.parametrize("candidate", [CAR.TOYOTA_RAV4_TSS2_2022, CAR.TOYOTA_RAV4_TSS2_2023])
def test_default_rav4_remains_vision_only(candidate):
CP = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], True, False, False, None)
radar = RadarInterface(CP)
assert CP.radarUnavailable
assert not CP.flags & (ToyotaFlags.ARS510_RADAR | ToyotaFlags.ARS510_FUSED)
assert radar.ars510 is radar.rcp is None
def set_bits(data, start, length, value):
raw = int.from_bytes(data, "little")
mask = ((1 << length) - 1) << start
data[:] = ((raw & ~mask) | (value << start)).to_bytes(len(data), "little")
def object_record(age=60, distance=30, lateral=0, speed_code=645, uncertainty=1, count=1):
record = bytearray(742)
record[0] = 0xe4
for slot in range(20):
data = bytearray(IDLE_SLOT)
if slot < count:
set_bits(data, 24, 7, age)
set_bits(data, 32, 12, round(160 + 16 * distance))
set_bits(data, 44, 12, round(2048 + lateral / 0.015))
set_bits(data, 64, 10, speed_code)
set_bits(data, 240, 7, uncertainty)
record[17 + slot * 36:17 + (slot + 1) * 36] = data
record[737:741] = zlib.crc32(record[1:737]).to_bytes(4, "little")
return bytes(record)
def record_frames(record, bus=1):
return [(0x80, bytes([0x12 if i == 0 else 0x20 | i % 16]) + record[i * 7:(i + 1) * 7], bus) for i in range(106)]
def ego_frames(speed=20, yaw_code=512):
speed_data = bytearray(8)
speed_data[5:7] = round(speed * 3.6 * 100).to_bytes(2, "big")
yaw_data = bytes([yaw_code >> 8, yaw_code & 0xff]) + bytes(6)
return [(0xb4, bytes(speed_data), 0), (0x24, yaw_data, 0)]
def fused_radar():
return FusedRadarInterface(structs.CarParams(carFingerprint=CAR.TOYOTA_RAV4_TSS2_2023))
def test_fused_decodes_20_slots_and_preserves_left_positive_geometry():
radar = fused_radar()
ret = radar.update([(1_000_000_000, ego_frames() + record_frames(object_record(count=20, lateral=0.3)))])
assert len(ret.points) == 20
assert len({pt.trackId for pt in ret.points}) == 20
for pt in ret.points:
assert (pt.dRel, pt.yRel, pt.vRel) == pytest.approx((30, 0.3, (645 - 510.5) * 0.149 - 20))
assert pt.measured
assert isnan(pt.aRel) and isnan(pt.yvRel)
assert not ret.errors.canError
@pytest.mark.parametrize("bad_input", ["crc", "short", "truncated", "reordered", "wrong_bus"])
def test_fused_rejects_incomplete_corrupt_or_non_native_records(bad_input):
radar = fused_radar()
record = bytearray(object_record())
if bad_input == "crc":
record[25] ^= 1
frames = record_frames(record, bus=2 if bad_input == "wrong_bus" else 1)
if bad_input == "short":
frames[50] = (0x80, frames[50][1][:-1], 1)
elif bad_input == "truncated":
frames.pop(50)
elif bad_input == "reordered":
frames[30], frames[31] = frames[31], frames[30]
assert radar.update([(1_000_000_000, ego_frames() + frames)]) is None
assert radar.last_record_t is None
def test_fused_requires_fresh_ego_speed_and_converged_tracks():
radar = Ars510Radar()
assert radar.points(1.0, object_record()) == []
radar.v_ego = (1.0, 20)
assert radar.points(1.0, object_record(age=59)) == []
assert radar.points(1.1, object_record(age=60))
assert radar.points(1.6, object_record(age=61)) == []
def test_fused_lifecycle_age_reset_allocates_a_new_track():
radar = Ars510Radar()
radar.v_ego = (1.0, 20)
first = radar.points(1.0, object_record(age=60))[0][0]
assert radar.points(1.06, object_record(age=61))[0][0] == first
radar.points(1.12, object_record(age=1))
radar.v_ego = (1.18, 20)
assert radar.points(1.18, object_record(age=60))[0][0] != first
saturated = radar.track_id(2.0, 0, 126)
assert radar.track_id(2.06, 0, 126) == saturated
@pytest.mark.parametrize("kwargs", [{"uncertainty": 127}, {"distance": 61}, {"lateral": 3}])
def test_fused_gates_uncertain_far_and_out_of_path_objects(kwargs):
radar = Ars510Radar()
radar.v_ego = (1.0, 20)
radar.yaw = (1.0, 0)
assert radar.points(1.0, object_record(**kwargs)) == []
def test_fused_damps_a_high_uncertainty_speed_excursion():
radar = Ars510Radar()
radar.v_ego = (1.0, 20)
first = radar.points(1.0, object_record(age=126))[0]
raw_closing = (600 - 510.5) * 0.149 - 20
second = radar.points(1.06, object_record(age=126, speed_code=600, uncertainty=127))[0]
assert raw_closing < -6
assert second[0] == first[0]
assert abs(second[3] - first[3]) < 0.2
def test_fused_keeps_healthy_tracks_between_scans_then_reports_actual_silence(monkeypatch):
radar = fused_radar()
monkeypatch.setattr(radar.radar, "update", lambda t, *_: [(1, 30, 0, -1)] if round(t * 100) % 6 == 0 else None)
for i in range(1, 101):
ret = radar.update([(i * 10_000_000, [(0x80, bytes(8), 1)])])
if i % 6 == 0:
assert len(ret.points) == 1
elif i > 6:
assert ret is None
monkeypatch.setattr(radar.radar, "update", lambda *_: None)
outputs = [radar.update([(2_000_000_000 + i * 10_000_000, [])]) for i in range(10)]
assert any(r is not None and r.errors.radarUnavailableTemporary and not r.points for r in outputs)
monkeypatch.setattr(radar.radar, "update", lambda *_: [])
ret = radar.update([(2_200_000_000, [(0x80, bytes(8), 1)])])
assert not ret.points and not ret.errors.radarUnavailableTemporary
def acc_frames(packer, counter, cycle, present=True, distance=35, relative_speed=-2, target_id=3):
return [packer.make_can_msg(name, 1, values | {"COUNTER": counter % 16}) for name, values in (
("ACC_TARGET_1", {"STATE": 5 if present else 1, "TARGET_ID": target_id, "REL_SPEED": relative_speed}),
("ACC_TARGET_2", {"PRESENT": int(present), "LONG_DIST": distance, "LAT_DIST": 0.25}),
("ACC_TARGET_3", {"TIMESTAMP": cycle}),
)]
def test_acc_target_decodes_one_lead_once_per_cycle_and_clears_no_target():
radar = AccRadarInterface(structs.CarParams())
packer = CANPacker(RADAR_DBC)
results = []
for i in range(30):
results.append(radar.update([(1_000_000_000 + i * 20_000_000, acc_frames(packer, i, 60000 * (1 + i // 3)))]))
reports = [result for result in results if result is not None]
assert len(reports) == 10
for result in reports:
assert not result.errors.canError
pt, = result.points
assert pt.trackId == 3
assert (pt.dRel, pt.yRel, pt.vRel) == pytest.approx((35, 0.25, -2))
assert pt.measured and isfinite(pt.vRel)
assert isnan(pt.aRel) and isnan(pt.yvRel)
ret = radar.update([(1_600_000_000, acc_frames(packer, 30, 660000, present=False))])
assert not ret.points and not ret.errors.canError
def test_acc_target_decodes_captured_rav4_hybrid_frame_bank():
# Stock RAV4 Hybrid 2022 route 2475fb3eb2ffcc2e/2022-04-29--12-46-23, segment 0.
frames = [(0x235, bytes.fromhex("af4563802b24040e"), 1),
(0x237, bytes.fromhex("ff404cc3c10a9550"), 1),
(0x239, bytes.fromhex("2a464008061677c8"), 1)]
ret = AccRadarInterface(structs.CarParams()).update([(1_000_000_000, frames)])
pt, = ret.points
assert not ret.errors.canError
assert pt.trackId == 14
assert (pt.dRel, pt.yRel, pt.vRel) == pytest.approx((3.825, 1.68, 0.125))
@pytest.mark.parametrize("bus,length", [(1, 7), (1, 9), (0, 8), (2, 8), (129, 8)])
def test_acc_target_ignores_malformed_frames_and_non_native_copies(bus, length):
radar = AccRadarInterface(structs.CarParams())
frames = [(addr, bytes(length), bus) for addr in (0x235, 0x237, 0x239)]
assert radar.update([(1_000_000_000, frames)]) is None
assert radar.last_record_t is None
@pytest.mark.parametrize("silent_clock", [False, True])
def test_acc_target_reports_silence_or_a_stuck_clock_without_stale_points(silent_clock):
radar = AccRadarInterface(structs.CarParams())
packer = CANPacker(RADAR_DBC)
assert radar.update([(1_000_000_000, acc_frames(packer, 0, 60000))]).points
outputs = []
for i in range(1, 51):
frames = acc_frames(packer, i, 60000) if silent_clock else []
outputs.append(radar.update([(1_000_000_000 + i * 20_000_000, frames)]))
assert any(r is not None and r.errors.radarUnavailableTemporary and not r.points for r in outputs)
@pytest.mark.parametrize("mode", [1, 2])
def test_rav4_startup_wait_does_not_stall_live_tracks(mode):
CP = CarInterface.get_params(CAR.TOYOTA_RAV4_TSS2_2023, gen_empty_fingerprint(), [], False, False, False,
SimpleNamespace(toyota_radar_mode_requested=mode))
radar = RadarInterface(CP)
outputs = [radar.update([(1_000_000_000 + i * 10_000_000, [])]) for i in range(700)]
assert sum(r is not None for r in outputs) == 140
assert all(not r.points and not r.errors.radarUnavailableTemporary for r in outputs if r is not None)
@@ -67,6 +67,8 @@ class ToyotaFlags(IntFlag):
DISABLE_RADAR = 4
# The DSU's ACC messages are rerouted through the camera bus by an adapter.
DSU_BYPASS = 8192
ARS510_RADAR = 16384
ARS510_FUSED = 32768
# Static flags
TSS2 = 8
@@ -619,6 +621,7 @@ UNSUPPORTED_DSU_CAR = CAR.with_flags(ToyotaFlags.UNSUPPORTED_DSU)
# these cars have a radar which sends ACC messages instead of the camera
RADAR_ACC_CAR = CAR.with_flags(ToyotaFlags.RADAR_ACC)
ARS510_RADAR_CAR = {CAR.TOYOTA_RAV4_TSS2_2022, CAR.TOYOTA_RAV4_TSS2_2023}
ANGLE_CONTROL_CAR = CAR.with_flags(ToyotaFlags.ANGLE_CONTROL)
@@ -250,6 +250,12 @@ BO_ 803 ES_Infotainment: 8 XXX
SG_ LKAS_State_Infotainment : 22|3@0+ (1,0) [0|7] "" XXX
SG_ Signal2 : 24|1@0+ (1,0) [0|1] "" XXX
BO_ 964 ES_LKAS_Alert: 8 XXX
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
SG_ Signal1 : 12|4@1+ (1,0) [0|15] "" XXX
SG_ Payload : 16|48@1+ (1,0) [0|281474976710655] "" XXX
BO_ 722 AC_State: 8 XXX
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
@@ -20,4 +20,4 @@ BO_ 513 GAS_SENSOR: 6 INTERCEPTOR
VAL_ 513 STATE 5 "FAULT_TIMEOUT" 4 "FAULT_STARTUP" 3 "FAULT_SCE" 2 "FAULT_SEND" 1 "FAULT_BAD_CHECKSUM" 0 "NO_FAULT" ;
CM_ "Kia Ray EV comma pedal uses the standard 0x200/0x201 rolling counter and CRC8 protocol. Channel scaling is Ray-only, estimated from the September 15 route: sensor rest raw 264/497, slopes about 3.79/7.68 raw counts per native E_EMS11 pedal unit, and 100 native units mapped to 255 comma pedal command units. Confirm against installed Ray firmware before increasing the command cap.";
CM_ "Kia Ray EV comma pedal uses the standard 0x200/0x201 rolling counter and CRC8 protocol. Command scaling is Ray-only, estimated from the September 15 route: zero command raw 264/497, slopes about 3.79/7.68 raw counts per native E_EMS11 pedal unit, and 100 native units mapped to 255 comma pedal command units. October ray_1800_M100 firmware transforms physical ADC inputs by 1.8 minus 100, but receives command tracks directly as DAC counts: do not apply that input transform to GAS_COMMAND. Sensor offsets depend on firmware; driver override uses raw tracks, not these physical signal values. Confirm against installed Ray firmware before increasing the command cap.";
@@ -254,6 +254,12 @@ BO_ 803 ES_Infotainment: 8 XXX
SG_ LKAS_State_Infotainment : 22|3@0+ (1,0) [0|7] "" XXX
SG_ Signal2 : 24|1@0+ (1,0) [0|1] "" XXX
BO_ 964 ES_LKAS_Alert: 8 XXX
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
SG_ Signal1 : 12|4@1+ (1,0) [0|15] "" XXX
SG_ Payload : 16|48@1+ (1,0) [0|281474976710655] "" XXX
BO_ 722 AC_State: 8 XXX
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
@@ -254,6 +254,12 @@ BO_ 803 ES_Infotainment: 8 XXX
SG_ LKAS_State_Infotainment : 22|3@0+ (1,0) [0|7] "" XXX
SG_ Signal2 : 24|1@0+ (1,0) [0|1] "" XXX
BO_ 964 ES_LKAS_Alert: 8 XXX
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
SG_ Signal1 : 12|4@1+ (1,0) [0|15] "" XXX
SG_ Payload : 16|48@1+ (1,0) [0|281474976710655] "" XXX
BO_ 722 AC_State: 8 XXX
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
@@ -0,0 +1,104 @@
VERSION ""
NS_ :
NS_DESC_
CM_
BA_DEF_
BA_
VAL_
CAT_DEF_
CAT_
FILTER
BA_DEF_DEF_
EV_DATA_
ENVVAR_DATA_
SGTYPE_
SGTYPE_VAL_
BA_DEF_SGTYPE_
BA_SGTYPE_
SIG_TYPE_REF_
VAL_TABLE_
SIG_GROUP_
SIG_VALTYPE_
SIGTYPE_VALTYPE_
BO_TX_BU_
BA_DEF_REL_
BA_REL_
BA_DEF_DEF_REL_
BU_SG_REL_
BU_EV_REL_
BU_BO_REL_
SG_MUL_VAL_
BS_:
BU_: XXX
BO_ 565 ACC_TARGET_1: 8 XXX
SG_ CHECK_BYTE : 7|8@0+ (1,0) [0|255] "" XXX
SG_ COUNTER : 15|4@0+ (1,0) [0|15] "" XXX
SG_ STATE : 11|4@0+ (1,0) [0|15] "" XXX
SG_ REL_ACCEL : 23|8@0+ (0.125,-12.5) [-12.5|19.375] "m/s^2" XXX
SG_ REL_SPEED : 31|11@0+ (0.125,-128) [-128|127.875] "m/s" XXX
SG_ LAT_ACCEL : 34|8@0+ (0.125,-12.5) [-12.5|19.375] "m/s^2" XXX
SG_ LAT_SPEED : 42|11@0+ (0.0125,-12.8) [-12.8|12.7875] "m/s" XXX
SG_ TARGET_ID : 60|5@0+ (1,0) [0|31] "" XXX
BO_ 567 ACC_TARGET_2: 8 XXX
SG_ CHECK_BYTE : 7|8@0+ (1,0) [0|255] "" XXX
SG_ COUNTER : 15|4@0+ (1,0) [0|15] "" XXX
SG_ LONG_DIST : 11|13@0+ (0.025,0) [0|204.775] "m" XXX
SG_ LAT_DIST : 30|12@0+ (0.01,-20) [-20|20.95] "m" XXX
SG_ TRACKER_A : 34|8@0+ (1,0) [0|255] "" XXX
SG_ TRACKER_B : 42|8@0+ (1,0) [0|255] "" XXX
SG_ IN_PATH : 48|1@0+ (1,0) [0|1] "" XXX
SG_ PRESENT : 50|1@0+ (1,0) [0|1] "" XXX
SG_ IN_PATH_LEVEL : 62|3@0+ (1,0) [0|7] "" XXX
SG_ IN_PATH_CANDIDATE : 63|1@0+ (1,0) [0|1] "" XXX
BO_ 569 ACC_TARGET_3: 8 XXX
SG_ CHECK_BYTE : 7|8@0+ (1,0) [0|255] "" XXX
SG_ COUNTER : 15|4@0+ (1,0) [0|15] "" XXX
SG_ CLASS : 11|4@0+ (1,0) [0|15] "" XXX
SG_ STATE : 23|2@0+ (1,0) [0|3] "" XXX
SG_ IN_OBJECT_LIST : 31|1@0+ (1,0) [0|1] "" XXX
SG_ PRESENT : 27|1@0+ (1,0) [0|1] "" XXX
SG_ TIMESTAMP : 26|32@0+ (1,0) [0|4294967295] "us" XXX
BO_ 571 ACC_TARGET_4: 3 XXX
SG_ CHECK_BYTE : 7|8@0+ (1,0) [0|255] "" XXX
SG_ COUNTER : 15|4@0+ (1,0) [0|15] "" XXX
SG_ WIDTH : 11|12@0+ (1,0) [0|4095] "cm" XXX
CM_ "Continental ARS510 front radar, radar bus: the target the radar's own ACC follows. Used on the radar-ACC platforms: RAV4 2022-25 (fwdRadar 8821F0R01100, 8821F0R03100), C-HR 2021-22 (8821FF4102xx-0500) and Yaris (8821F0D0xxxx). The 0x80 object list and 0x85 metadata record span many frames and are decoded by ars510-radar (ars510/). The radar interface uses STATE, REL_SPEED, TARGET_ID, LONG_DIST, LAT_DIST, PRESENT and TIMESTAMP; the other signals are documented for completeness.";
CM_ BO_ 565 "ACC target, part 1: 50 Hz, content updated once per 60 ms radar cycle.";
CM_ SG_ 565 CHECK_BYTE "Check byte over the rest of the frame (algorithm unknown).";
CM_ SG_ 565 STATE "5 = target present, 1 = idle (bytes 2-7 64 80 0B 24 00 FF), 15 = start-up.";
CM_ SG_ 565 REL_ACCEL "Target acceleration relative to ego, positive = opening.";
CM_ SG_ 565 REL_SPEED "Closing speed relative to ego, negative = closing.";
CM_ SG_ 565 LAT_ACCEL "Relative lateral acceleration, left positive (nominal unit). Mostly minus ego's own lateral acceleration.";
CM_ SG_ 565 LAT_SPEED "Lateral speed of the target relative to ego, left positive (about 0.0125 m/s per code).";
CM_ SG_ 565 TARGET_ID "Changes when the radar switches to a different target; 31 while idle.";
CM_ BO_ 567 "ACC target, part 2: 50 Hz.";
CM_ SG_ 567 CHECK_BYTE "Check byte: an affine parity function of bytes 1-7.";
CM_ SG_ 567 LONG_DIST "Distance from the radar.";
CM_ SG_ 567 LAT_DIST "Lateral position, left positive.";
CM_ SG_ 567 TRACKER_A "Raw tracker byte: about 35 on a steady target, higher while it accelerates or brakes.";
CM_ SG_ 567 TRACKER_B "Raw tracker byte: about 86 for a stopped target, 128-135 for a steady mover, rises (and wraps) while it accelerates or brakes.";
CM_ SG_ 567 IN_PATH "1 while the target is confirmed in the path (level 5).";
CM_ SG_ 567 PRESENT "1 = target present.";
CM_ SG_ 567 IN_PATH_LEVEL "In-path level 5, 4, 3, 2; steps to the neighboring level only; lower levels sit farther from the path.";
CM_ SG_ 567 IN_PATH_CANDIDATE "1 while the target is a candidate (levels 4-2).";
CM_ BO_ 569 "ACC target, part 3: 50 Hz.";
CM_ SG_ 569 CHECK_BYTE "Check byte: an affine parity function of bytes 1-7.";
CM_ SG_ 569 CLASS "Target class: 1 car, 2 truck, 0 / 4 / 5 / 6 other, 7 idle, 15 start-up.";
CM_ SG_ 569 STATE "1 with an ACC target, 2 idle.";
CM_ SG_ 569 IN_OBJECT_LIST "1 while the target also has an object in the 0x80 object list.";
CM_ SG_ 569 PRESENT "1 = target present.";
CM_ SG_ 569 TIMESTAMP "Radar cycle clock: changes once per 60 ms radar cycle, when the ACC target is recomputed.";
CM_ BO_ 571 "ACC target, part 4: 50 Hz, 3 bytes.";
CM_ SG_ 571 CHECK_BYTE "CRC-8 (polynomial 0x1D, init 0, xor 0x59) over the counter and width bits.";
CM_ SG_ 571 WIDTH "Target width: 0 while idle, 140-220 cm for the car class, 200-280 cm for the truck class.";
VAL_ 565 STATE 15 "STARTUP" 5 "TARGET" 1 "NO_TARGET" ;
VAL_ 569 CLASS 0 "OTHER_0" 1 "CAR" 2 "TRUCK" 4 "OTHER_4" 5 "OTHER_5" 6 "OTHER_6" 7 "NO_TARGET" 15 "STARTUP" ;
VAL_ 569 STATE 1 "TARGET" 2 "NO_TARGET" ;
+1 -1
View File
@@ -330,7 +330,7 @@ static bool hyundai_tx_hook(const CANPacket_t *msg) {
const int expected_track2 = 497 + (2 * ((int)track1 - 264));
if ((msg->data[4] & 0x70U) != 0U ||
(msg->data[5] != hyundai_ray_pedal_checksum(msg)) ||
(enabled && (track1 < 264U || track1 > 473U || track2 < 497U || track2 > 919U ||
(enabled && (track1 < 264U || track1 > 530U || track2 < 497U || track2 > 1035U ||
SAFETY_ABS((int)track2 - expected_track2) > 40)) ||
(!enabled && ((track1 != 0U) || (track2 != 0U))) ||
longitudinal_interceptor_checks(msg) ||
+16 -1
View File
@@ -35,6 +35,7 @@
#define MSG_SUBARU_ES_DashStatus 0x321U
#define MSG_SUBARU_ES_LKAS_State 0x322U
#define MSG_SUBARU_ES_Infotainment 0x323U
#define MSG_SUBARU_ES_LKAS_Alert 0x3C4U
#define MSG_SUBARU_Cruise_Buttons 0x146U
#define MSG_SUBARU_ES_UDS_Request 0x787U
@@ -304,6 +305,10 @@ static bool subaru_tx_hook(const CANPacket_t *msg) {
violation |= subaru_get_checksum(msg) != subaru_compute_checksum(msg);
}
if (msg->addr == MSG_SUBARU_ES_LKAS_Alert) {
violation |= subaru_get_checksum(msg) != subaru_compute_checksum(msg);
}
if (msg->addr == MSG_SUBARU_Cruise_Buttons) {
violation |= !subaru_redneck_cruise;
violation |= msg->bus != SUBARU_MAIN_BUS;
@@ -377,6 +382,12 @@ static safety_config subaru_init(uint16_t param) {
SUBARU_COMMON_TX_MSGS(SUBARU_ALT_BUS)
};
static const CanMsg SUBARU_ASCENT_LKAS_ALERT_TX_MSGS[] = {
SUBARU_BASE_TX_MSGS(SUBARU_ALT_BUS, MSG_SUBARU_ES_LKAS_ANGLE)
SUBARU_COMMON_TX_MSGS(SUBARU_ALT_BUS)
{MSG_SUBARU_ES_LKAS_Alert, SUBARU_MAIN_BUS, 8, .check_relay = true},
};
static const CanMsg SUBARU_GEN2_LKAS_ANGLE_STOP_START_TX_MSGS[] = {
SUBARU_BASE_TX_MSGS(SUBARU_ALT_BUS, MSG_SUBARU_ES_LKAS_ANGLE)
SUBARU_COMMON_TX_MSGS(SUBARU_ALT_BUS)
@@ -444,6 +455,9 @@ static safety_config subaru_init(uint16_t param) {
const uint16_t SUBARU_PARAM_REDNECK_CRUISE = 512;
subaru_redneck_cruise = GET_FLAG(param, SUBARU_PARAM_REDNECK_CRUISE);
const uint16_t SUBARU_PARAM_ASCENT_LKAS_ALERT = 2048;
const bool subaru_ascent_lkas_alert = GET_FLAG(param, SUBARU_PARAM_ASCENT_LKAS_ALERT);
#ifdef ALLOW_DEBUG
const uint16_t SUBARU_PARAM_LONGITUDINAL = 2;
subaru_longitudinal = GET_FLAG(param, SUBARU_PARAM_LONGITUDINAL);
@@ -454,7 +468,8 @@ static safety_config subaru_init(uint16_t param) {
ret = subaru_d_platform ? (subaru_stop_start_button ? BUILD_SAFETY_CFG(subaru_d_platform_angle_rx_checks, SUBARU_D_PLATFORM_ANGLE_STOP_START_MAIN_TX_MSGS) : \
(subaru_d_platform_camera ? BUILD_SAFETY_CFG(subaru_d_platform_angle_rx_checks, SUBARU_D_PLATFORM_ANGLE_CAMERA_TX_MSGS) : \
BUILD_SAFETY_CFG(subaru_d_platform_angle_rx_checks, SUBARU_D_PLATFORM_ANGLE_MAIN_TX_MSGS))) : \
subaru_gen2 ? (subaru_stop_start_button ? BUILD_SAFETY_CFG(subaru_gen2_lkas_angle_rx_checks, SUBARU_GEN2_LKAS_ANGLE_STOP_START_TX_MSGS) : \
subaru_gen2 ? (subaru_ascent_lkas_alert ? BUILD_SAFETY_CFG(subaru_gen2_lkas_angle_rx_checks, SUBARU_ASCENT_LKAS_ALERT_TX_MSGS) : \
subaru_stop_start_button ? BUILD_SAFETY_CFG(subaru_gen2_lkas_angle_rx_checks, SUBARU_GEN2_LKAS_ANGLE_STOP_START_TX_MSGS) : \
BUILD_SAFETY_CFG(subaru_gen2_lkas_angle_rx_checks, SUBARU_GEN2_LKAS_ANGLE_TX_MSGS)) : \
BUILD_SAFETY_CFG(subaru_lkas_angle_rx_checks, SUBARU_LKAS_ANGLE_TX_MSGS);
} else if (subaru_gen2) {
@@ -22,8 +22,9 @@ def test_ray_pedal_tx_isolation_and_limits(param):
has_ray_signature = param in (0x9405, 0x9C05)
assert tx(0) is has_ray_signature
assert tx(0.55) is has_ray_signature
assert not tx(0.56)
assert not tx(0.70)
assert tx(0.65) is has_ray_signature
assert tx(0.70) is has_ray_signature
assert not tx(0.71)
assert not tx(1.0)
if has_ray_signature:
@@ -51,6 +52,24 @@ def test_ray_pedal_rx_crc_is_checked_only_for_ray_signature():
assert not safety.safety_rx_hook(libsafety_py.make_CANPacket(0x201, 0, bytes(bad_crc)))
@pytest.mark.parametrize(("track1", "track2", "allowed"), [
(530, 1035, True), (531, 1035, False), (530, 1036, False),
(263, 497, False), (264, 496, False), (530, 980, False),
])
def test_ray_tuned_command_validates_each_track(track1, track2, allowed):
safety = libsafety_py.libsafety
safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x9405)
safety.init_tests()
safety.set_controls_allowed(True)
packer = CANPacker("hyundai_kia_ray_pedal")
addr, dat, bus = packer.make_can_msg("GAS_COMMAND", 0, {
"GAS_COMMAND": (track1 - 264) * 0.672,
"GAS_COMMAND2": (track2 - 497) * 0.332,
"ENABLE": 1, "COUNTER_PEDAL": 1,
})
assert safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, dat)) is allowed
def test_ray_native_commanded_gas_does_not_cancel_driver_override_safety():
safety = libsafety_py.libsafety
safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x9405)
@@ -32,6 +32,7 @@ class SubaruMsg(enum.IntEnum):
ES_DashStatus = 0x321
ES_LKAS_State = 0x322
ES_Infotainment = 0x323
ES_LKAS_Alert = 0x3C4
ES_UDS_Request = 0x787
ES_HighBeamAssist = 0x121
ES_STATIC_1 = 0x22a
@@ -385,6 +386,32 @@ class TestSubaruGen2FixedAngleStopStartSafety(TestSubaruGen2FixedAngleSafety):
self.assertFalse(self._tx(self._stop_start_msg(False)))
class TestSubaruGen2AscentLkasAlertSafety(TestSubaruGen2FixedAngleSafety):
FLAGS = TestSubaruGen2FixedAngleSafety.FLAGS | SubaruSafetyFlags.ASCENT_LKAS_ALERT
TX_MSGS = TestSubaruGen2FixedAngleSafety.TX_MSGS + [[SubaruMsg.ES_LKAS_Alert, SUBARU_MAIN_BUS]]
RELAY_MALFUNCTION_ADDRS = {SUBARU_MAIN_BUS: (*TestSubaruAngleSafetyBase.RELAY_MALFUNCTION_ADDRS[SUBARU_MAIN_BUS],
SubaruMsg.ES_LKAS_Alert)}
FWD_BLACKLISTED_ADDRS = {SUBARU_CAM_BUS: (*TestSubaruAngleSafetyBase.FWD_BLACKLISTED_ADDRS[SUBARU_CAM_BUS],
SubaruMsg.ES_LKAS_Alert)}
def test_secondary_alert_tx_requires_checksum(self):
from opendbc.safety.tests.libsafety.libsafety_py import make_CANPacket
self.assertTrue(self._tx(make_CANPacket(SubaruMsg.ES_LKAS_Alert, SUBARU_MAIN_BUS, bytes.fromhex("ce02000104000000"))))
self.assertFalse(self._tx(make_CANPacket(SubaruMsg.ES_LKAS_Alert, SUBARU_MAIN_BUS, bytes.fromhex("0002000104000000"))))
def test_secondary_alert_permission_and_forwarding_are_flag_gated(self):
from opendbc.safety.tests.libsafety.libsafety_py import make_CANPacket
msg = make_CANPacket(SubaruMsg.ES_LKAS_Alert, SUBARU_MAIN_BUS, bytes.fromhex("ce02000104000000"))
for flags in (0, SubaruSafetyFlags.GEN2, SubaruSafetyFlags.LKAS_ANGLE, TestSubaruGen2FixedAngleSafety.FLAGS,
TestSubaruDPlatformAngleSafety.FLAGS, TestSubaruDPlatformCameraAngleSafety.FLAGS,
SubaruSafetyFlags.ASCENT_LKAS_ALERT):
with self.subTest(flags=flags):
self.safety.set_safety_hooks(CarParams.SafetyModel.subaru, flags)
self.safety.init_tests()
self.assertFalse(self._tx(msg))
self.assertEqual(self.safety.safety_fwd_hook(SUBARU_CAM_BUS, SubaruMsg.ES_LKAS_Alert), SUBARU_MAIN_BUS)
class TestSubaruDPlatformAngleSafety(TestSubaruStockLongitudinalSafetyBase, TestSubaruAngleSafetyBase):
FLAGS = SubaruSafetyFlags.GEN2 | SubaruSafetyFlags.LKAS_ANGLE | SubaruSafetyFlags.D_PLATFORM
ALT_MAIN_BUS = SUBARU_ALT_BUS
+5
View File
@@ -1,3 +1,8 @@
#pragma once
#define SPI_CAN_WRITE_SEQUENCED_ENDPOINT 0x83U
#define SPI_CAN_REPLAY_CAPABILITY 0x5351U
typedef struct {
uint8_t request;
uint16_t param1;
+4
View File
@@ -2,6 +2,7 @@
#include "board/drivers/spi_declarations.h"
#include "board/crc.h"
#include "board/drivers/spi_can_replay.h"
uint8_t spi_buf_rx[SPI_BUF_SIZE];
uint8_t spi_buf_tx[SPI_BUF_SIZE];
@@ -130,6 +131,9 @@ void spi_rx_done(void) {
} else if (spi_endpoint == 2U) {
comms_endpoint2_write(&spi_buf_rx[SPI_HEADER_SIZE], spi_data_len_mosi);
response_ack = true;
} else if (spi_endpoint == SPI_CAN_WRITE_SEQUENCED_ENDPOINT) {
response_ack = spi_can_write_sequenced(spi_data_len_miso, &spi_buf_rx[SPI_HEADER_SIZE], spi_data_len_mosi,
&spi_can_tx_ready);
} else if (spi_endpoint == 3U) {
if (spi_data_len_mosi > 0U) {
if (spi_can_tx_ready) {
+34
View File
@@ -0,0 +1,34 @@
#pragma once
typedef struct {
uint16_t sequence;
uint16_t length;
bool valid;
uint8_t data[SPI_BUF_SIZE];
} spi_can_replay_t;
static spi_can_replay_t spi_can_replay;
static inline void spi_can_replay_reset(void) {
spi_can_replay.valid = false;
}
static inline bool spi_can_write_sequenced(uint16_t sequence, const uint8_t *data, uint16_t length, bool *tx_ready) {
bool accepted = false;
if ((sequence != 0U) && (length > 0U) && (length < SPI_BUF_SIZE)) {
if (spi_can_replay.valid && (sequence == spi_can_replay.sequence)) {
// A lost reply must not turn an accepted CAN write into a second transmission.
accepted = (length == spi_can_replay.length) && (memcmp(data, spi_can_replay.data, length) == 0);
} else if (*tx_ready) {
*tx_ready = false;
comms_can_write(data, length);
(void)memcpy(spi_can_replay.data, data, length);
spi_can_replay.sequence = sequence;
spi_can_replay.length = length;
spi_can_replay.valid = true;
accepted = true;
} else {
}
}
return accepted;
}
+8
View File
@@ -221,6 +221,14 @@ int comms_control_handler(ControlPacket_t *req, uint8_t *resp) {
break;
// **** 0xdd: get healthpacket and CANPacket versions
case 0xdd:
#ifdef STM32H7
if (req->param1 == SPI_CAN_REPLAY_CAPABILITY) {
if (req->param2 == 1U) spi_can_replay_reset();
resp[0] = 1U;
resp_len = 1;
break;
}
#endif
resp[0] = HEALTH_PACKET_VERSION;
resp[1] = CAN_PACKET_VERSION;
resp[2] = CAN_HEALTH_PACKET_VERSION;
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,2 +1,2 @@
extern const uint8_t gitversion[19];
const uint8_t gitversion[19] = "DEV-956aceaf-DEBUG";
const uint8_t gitversion[19] = "DEV-b220a37f-DEBUG";
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
DEV-956aceaf-DEBUG
DEV-b220a37f-DEBUG
+36 -5
View File
@@ -4,7 +4,7 @@ from collections import deque
from cereal import custom, log
from opendbc.car.honda.values import CAR as HONDA_CAR, HondaFlags
from opendbc.car.hyundai.values import HyundaiFlags
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, HyundaiFlags
from opendbc.car.lateral import get_friction
from opendbc.car.toyota.values import CAR as TOYOTA_CAR
from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY
@@ -13,7 +13,12 @@ from openpilot.common.pid import PIDController
from openpilot.selfdrive.controls.lib.drive_helpers import MIN_SPEED
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
from openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes import * # noqa: F403
from openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes import get_genesis_g70_center_measurement_damping_gain
from openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes import (
KIA_NIRO_EV_CARS,
get_genesis_g70_center_measurement_damping_gain,
get_genesis_g70_traffic_friction_increment,
get_kia_niro_ev_friction_threshold,
)
# At higher speeds (25+mph) we can assume:
# Lateral acceleration achieved by a specific car correlates to
@@ -129,6 +134,7 @@ class LatControlTorque(LatControl):
self.is_sonata_hybrid = CP.carFingerprint in SONATA_HYBRID_CARS
self.is_elantra_non_scc = CP.carFingerprint in ELANTRA_NON_SCC_CARS
self.is_kia_xceed = CP.carFingerprint in KIA_XCEED_CARS
self.is_kia_niro_ev = CP.carFingerprint in KIA_NIRO_EV_CARS
self.is_kia_niro_phev_2022 = CP.carFingerprint in KIA_NIRO_PHEV_2022_CARS
self.is_kia_stinger_2022 = CP.carFingerprint in KIA_STINGER_2022_CARS
self.is_kia_forte = CP.carFingerprint in KIA_FORTE_CARS
@@ -136,6 +142,7 @@ class LatControlTorque(LatControl):
self.is_kona_ev_2022 = CP.carFingerprint in KONA_EV_2022_CARS
self.is_kia_ev6 = CP.carFingerprint in KIA_EV6_CARS
self.is_kia_carnival = CP.carFingerprint in KIA_CARNIVAL_CARS
self.is_kia_carnival_hev = CP.carFingerprint == HYUNDAI_CAR.KIA_CARNIVAL_HEV_4TH_GEN
self.is_tucson_4th_gen = CP.carFingerprint in TUCSON_4TH_GEN_CARS
self.is_civic_bosch_modified = CP.carFingerprint == HONDA_CAR.HONDA_CIVIC_BOSCH and bool(CP.flags & HondaFlags.EPS_MODIFIED)
self.is_honda_accord = CP.carFingerprint == HONDA_CAR.HONDA_ACCORD
@@ -296,7 +303,11 @@ class LatControlTorque(LatControl):
)
# do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly
pid_log.error = float(error_with_lsf)
carnival_hev_center_weight = (
get_kia_carnival_hev_center_weight(CS.vEgo, setpoint, measurement, desired_lateral_jerk, future_desired_lateral_accel)
if self.is_kia_carnival_hev and not CS.steeringPressed and not self.prev_steering_pressed else 0.0
)
pid_log.error = float(error_with_lsf * (1.0 - KIA_CARNIVAL_HEV_CENTER_ERROR_REDUCTION * carnival_hev_center_weight))
ff = gravity_adjusted_future_lateral_accel
# latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll
ff -= self.torque_params.latAccelOffset * roll_offset_fade
@@ -369,10 +380,18 @@ class LatControlTorque(LatControl):
friction_threshold = get_standard_friction_threshold(CS.vEgo)
if self.is_genesis_g70:
friction_threshold = get_genesis_g70_friction_threshold(CS.vEgo, setpoint, desired_lateral_jerk)
if not CS.steeringPressed and not self.prev_steering_pressed:
friction_threshold += get_genesis_g70_traffic_friction_increment(
CS.vEgo, setpoint, desired_lateral_jerk, desired_curvature, measured_curvature,
)
elif self.is_genesis_gv70:
friction_threshold = get_genesis_gv70_friction_threshold(CS.vEgo, setpoint, desired_lateral_jerk)
elif self.is_sonata_hybrid:
friction_threshold = get_sonata_hybrid_friction_threshold(CS.vEgo, setpoint)
elif self.is_kia_niro_ev and not CS.steeringPressed:
friction_threshold = get_kia_niro_ev_friction_threshold(
CS.vEgo, setpoint, desired_lateral_jerk, future_desired_lateral_accel,
)
friction_scale = 1.0
if bolt_2022_2023_tuned_path_active:
ff *= get_bolt_2022_2023_ff_scale(setpoint, desired_lateral_jerk, CS.vEgo)
@@ -438,6 +457,10 @@ class LatControlTorque(LatControl):
if not self.is_ioniq_6_2025:
ff *= get_ioniq_6_2023_unwind_ff_scale(setpoint, measurement, desired_lateral_jerk, CS.vEgo)
friction_threshold = get_ioniq_6_friction_threshold(CS.vEgo, setpoint, desired_lateral_jerk) / max(ioniq_6_center_taper, 1e-3)
if not self.is_ioniq_6_2025 and not CS.steeringPressed and not self.prev_steering_pressed:
friction_threshold += get_ioniq_6_2023_center_friction_increment(
CS.vEgo, setpoint, measurement, desired_lateral_jerk, future_desired_lateral_accel,
) / max(ioniq_6_center_taper, 1e-3)
friction_scale = get_ioniq_6_friction_scale(CS.vEgo, setpoint, desired_lateral_jerk)
friction_scale = 1.0 + ((friction_scale - 1.0) * ioniq_6_center_taper)
friction_scale *= get_ioniq_6_friction_center_fade_scale(setpoint, CS.vEgo)
@@ -454,7 +477,10 @@ class LatControlTorque(LatControl):
elif kia_niro_phev_2022_active:
friction_threshold = get_kia_niro_phev_2022_friction_threshold(CS.vEgo, setpoint, desired_lateral_jerk)
elif kia_stinger_2022_active:
friction_threshold = get_kia_stinger_2022_friction_threshold(CS.vEgo, setpoint, desired_lateral_jerk)
friction_threshold = get_kia_stinger_2022_friction_threshold(
CS.vEgo, setpoint, desired_lateral_jerk, future_desired_lateral_accel,
CS.steeringPressed or self.prev_steering_pressed,
)
elif kia_forte_active:
ff *= get_kia_forte_ff_scale(setpoint, desired_lateral_jerk, CS.vEgo) * kia_forte_center_taper
friction_threshold = get_kia_forte_friction_threshold(CS.vEgo, setpoint, desired_lateral_jerk)
@@ -543,6 +569,7 @@ class LatControlTorque(LatControl):
)
friction_jerk = math.copysign(max(abs(desired_lateral_jerk) - friction_jerk_deadzone, 0.0),
desired_lateral_jerk)
friction_threshold += KIA_CARNIVAL_HEV_CENTER_FRICTION_INCREMENT * carnival_hev_center_weight
ff += friction_scale * get_friction(error_with_lsf + JERK_GAIN * friction_jerk, lateral_accel_deadzone, friction_threshold, self.torque_params)
deadzone_boost_active = False
if self.torque_deadzone_boost > 0.0 and abs(gravity_adjusted_future_lateral_accel) < DEADZONE_BOOST_LAT_ACCEL:
@@ -615,7 +642,10 @@ class LatControlTorque(LatControl):
low_speed_output_limit,
))
elif ioniq_5_active:
low_speed_output_limit = get_ioniq_5_low_speed_output_limit(setpoint, desired_lateral_jerk, CS.vEgo)
low_speed_output_limit = get_ioniq_5_low_speed_output_limit(
setpoint, desired_lateral_jerk, CS.vEgo,
desired_curvature if not CS.steeringPressed and not self.prev_steering_pressed else 0.0, output_torque,
)
output_torque = float(np.clip(
output_torque,
-low_speed_output_limit,
@@ -661,6 +691,7 @@ class LatControlTorque(LatControl):
output_torque *= kia_carnival_center_taper
output_torque *= get_kia_carnival_unwind_output_scale(
setpoint, measurement, desired_lateral_jerk, CS.vEgo,
output_torque=output_torque if self.is_kia_carnival_hev and not CS.steeringPressed and not self.prev_steering_pressed else None,
)
output_torque *= get_kia_carnival_highway_transition_output_scale(setpoint, desired_lateral_jerk, CS.vEgo)
elif palisade_active:
@@ -128,7 +128,8 @@ def is_ioniq_6_2025_model(CP) -> bool:
try:
for fw in CP.carFw:
value = fw.fwVersion
versions.append(value.decode("ascii", errors="ignore") if isinstance(value, bytes) else str(value))
versions.append(bytes(value).decode("ascii", errors="ignore")
if isinstance(value, (bytes, bytearray, memoryview)) else str(value))
except (AttributeError, TypeError, ValueError):
return False
@@ -158,6 +159,9 @@ TUCSON_4TH_GEN_CARS = (
KIA_XCEED_CARS = (
HYUNDAI_CAR.KIA_XCEED_PHEV,
)
KIA_NIRO_EV_CARS = (
HYUNDAI_CAR.KIA_NIRO_EV,
)
KIA_NIRO_PHEV_2022_CARS = (
HYUNDAI_CAR.KIA_NIRO_PHEV_2022,
)
@@ -240,6 +244,8 @@ GENESIS_GV70_UNWIND_FF_JERK = 0.10
GENESIS_GV70_UNWIND_FF_JERK_WIDTH = 0.10
GENESIS_GV70_UNWIND_FF_SPEED = 10.0 * CV.MPH_TO_MS
GENESIS_GV70_UNWIND_FF_SPEED_WIDTH = 4.0 * CV.MPH_TO_MS
GENESIS_GV70_CURVE_DAMPING_LAT_BP = [0.25, 0.55]
GENESIS_GV70_CURVE_DAMPING_OVERSHOOT_BP = [0.0, 0.20]
GENESIS_GV70_HIGH_SPEED_ERROR_DAMPING_MAX = 0.20
GENESIS_GV70_HIGH_SPEED_ERROR_DAMPING_SPEED = 50.0 * CV.MPH_TO_MS
GENESIS_GV70_HIGH_SPEED_ERROR_DAMPING_SPEED_WIDTH = 8.0 * CV.MPH_TO_MS
@@ -344,6 +350,11 @@ GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_LAT = 0.14
GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_LAT_WIDTH = 0.05
GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_SPEED = 6.0
GENESIS_G70_LOW_SPEED_OUTPUT_LIMIT_SPEED_WIDTH = 1.5
GENESIS_G70_TRAFFIC_FRICTION_INCREMENT = 0.90
GENESIS_G70_TRAFFIC_FRICTION_SPEED_BP = [6.0, 8.0]
GENESIS_G70_TRAFFIC_FRICTION_LAT_BP = [0.12, 0.30]
GENESIS_G70_TRAFFIC_FRICTION_CURVATURE_BP = [0.0035, 0.0080]
GENESIS_G70_TRAFFIC_FRICTION_JERK_BP = [0.15, 0.50]
GENESIS_G70_UNWIND_FF_REDUCTION_MAX = 0.34
GENESIS_G70_UNWIND_FF_OVERSHOOT = 0.13
GENESIS_G70_UNWIND_FF_OVERSHOOT_WIDTH = 0.17
@@ -607,6 +618,11 @@ KIA_XCEED_CENTER_TAPER_LAT_WIDTH = 0.03
KIA_XCEED_CENTER_TAPER_SPEED = 17.5
KIA_XCEED_CENTER_TAPER_SPEED_WIDTH = 2.5
KIA_NIRO_EV_CENTER_FRICTION_SPEED_BP = [60.0 * CV.MPH_TO_MS, 65.0 * CV.MPH_TO_MS]
KIA_NIRO_EV_CENTER_FRICTION_LAT_BP = [0.15, 0.35]
KIA_NIRO_EV_CENTER_FRICTION_JERK_BP = [0.20, 0.50]
KIA_NIRO_EV_CENTER_FRICTION_THRESHOLD_GAIN = 0.35
KIA_NIRO_PHEV_2022_CENTER_TAPER_MAX = 0.06
KIA_NIRO_PHEV_2022_CENTER_TAPER_LAT = 0.12
KIA_NIRO_PHEV_2022_CENTER_TAPER_LAT_WIDTH = 0.03
@@ -626,6 +642,10 @@ KIA_STINGER_2022_CENTER_TAPER_LAT_WIDTH = 0.05
KIA_STINGER_2022_CENTER_TAPER_SPEED = 10.0
KIA_STINGER_2022_CENTER_TAPER_SPEED_WIDTH = 2.5
KIA_STINGER_2022_FRICTION_THRESHOLD_GAIN = 0.10
KIA_STINGER_2022_HIGHWAY_FRICTION_INCREMENT = 0.20
KIA_STINGER_2022_HIGHWAY_FRICTION_SPEED_BP = [50.0 * CV.MPH_TO_MS, 60.0 * CV.MPH_TO_MS]
KIA_STINGER_2022_HIGHWAY_FRICTION_LAT_BP = [0.15, 0.40]
KIA_STINGER_2022_HIGHWAY_FRICTION_JERK_BP = [0.20, 0.75]
KIA_CARNIVAL_CENTER_TAPER_MAX = 0.20
KIA_CARNIVAL_CENTER_TAPER_LAT = 0.20
@@ -635,6 +655,12 @@ KIA_CARNIVAL_CENTER_TAPER_SPEED_WIDTH = 1.8
KIA_CARNIVAL_CENTER_TAPER_SPEED_MAX = 14.5
KIA_CARNIVAL_CENTER_TAPER_SPEED_MAX_WIDTH = 2.0
KIA_CARNIVAL_FRICTION_THRESHOLD_GAIN = 0.24
KIA_CARNIVAL_HEV_CENTER_SPEED_BP = [1.0, 4.0, 20.0, 25.0]
KIA_CARNIVAL_HEV_CENTER_LAT_BP = [0.18, 0.40]
KIA_CARNIVAL_HEV_CENTER_ERROR_BP = [0.12, 0.30]
KIA_CARNIVAL_HEV_CENTER_JERK_BP = [0.25, 0.75]
KIA_CARNIVAL_HEV_CENTER_FRICTION_INCREMENT = 0.20
KIA_CARNIVAL_HEV_CENTER_ERROR_REDUCTION = 0.25
KIA_CARNIVAL_FRICTION_CENTER_FADE_MAX = 0.34
KIA_CARNIVAL_HIGHWAY_CENTER_TAPER_MAX = 0.14
KIA_CARNIVAL_HIGHWAY_CENTER_TAPER_LAT = 0.24
@@ -810,6 +836,8 @@ IONIQ_5_SUSTAINED_TURN_IN_FF_LAT_END = 3.60
IONIQ_5_SUSTAINED_TURN_IN_FF_LAT_WIDTH = 0.30
IONIQ_5_LOW_SPEED_OUTPUT_LIMIT_BASE = 0.05
IONIQ_5_LOW_SPEED_OUTPUT_LIMIT_TURN_RELIEF = 0.95
IONIQ_5_LOW_SPEED_TURN_OUTPUT_FLOOR = 0.30
IONIQ_5_LOW_SPEED_TURN_CURVATURE_BP = [0.005, 0.020]
IONIQ_5_LOW_SPEED_OUTPUT_LIMIT_SPEED = 8.0
IONIQ_5_LOW_SPEED_OUTPUT_LIMIT_SPEED_WIDTH = 0.8
IONIQ_5_LOW_SPEED_CENTER_LAT = 0.40
@@ -982,6 +1010,11 @@ IONIQ_6_2023_UNWIND_FF_SPEED_ONSET = 8.0
IONIQ_6_2023_UNWIND_FF_SPEED_ONSET_WIDTH = 2.5
IONIQ_6_2023_UNWIND_FF_SPEED_CUTOFF = 23.5
IONIQ_6_2023_UNWIND_FF_SPEED_CUTOFF_WIDTH = 2.0
IONIQ_6_2023_CENTER_FRICTION_INCREMENT = 0.24
IONIQ_6_2023_CENTER_FRICTION_SPEED_BP = [3.5, 8.0]
IONIQ_6_2023_CENTER_FRICTION_LAT_BP = [0.12, 0.35]
IONIQ_6_2023_CENTER_FRICTION_ERROR_BP = [0.08, 0.18]
IONIQ_6_2023_CENTER_FRICTION_JERK_BP = [0.20, 0.60]
IONIQ_6_LOW_SPEED_PID_RESET_SPEED = 0.1 * CV.MPH_TO_MS
# Friction compensation near zero lateral accel amplifies planner jerk noise into a slow
# (~0.5 Hz) weave on straights: the 0.09/0.39 small-signal slope plus the jerk feed acts as
@@ -1342,9 +1375,9 @@ KONA_EV_2022_CENTER_FRICTION_THRESHOLD_SPEED = 18.0
KONA_EV_2022_CENTER_FRICTION_THRESHOLD_SPEED_WIDTH = 2.5
KONA_EV_2022_HIGH_SPEED_FRICTION_THRESHOLD_GAIN = 0.30
KONA_EV_2022_HIGH_SPEED_FRICTION_THRESHOLD_SPEED_ONSET = 100.0 / 3.6
KONA_EV_2022_HIGH_SPEED_FRICTION_THRESHOLD_SPEED_FULL = 120.0 / 3.6
KONA_EV_2022_HIGH_SPEED_FRICTION_THRESHOLD_LAT_FADE_START = 0.80
KONA_EV_2022_HIGH_SPEED_FRICTION_THRESHOLD_LAT_FADE_END = 1.50
KONA_EV_2022_HIGH_SPEED_FRICTION_THRESHOLD_SPEED_FULL = 110.0 / 3.6
KONA_EV_2022_HIGH_SPEED_FRICTION_THRESHOLD_LAT_FADE_START = 1.20
KONA_EV_2022_HIGH_SPEED_FRICTION_THRESHOLD_LAT_FADE_END = 2.00
KONA_EV_2022_CENTER_OUTPUT_TAPER_MAX = 0.08
KONA_EV_2022_CENTER_OUTPUT_TAPER_LAT = 0.20
KONA_EV_2022_CENTER_OUTPUT_TAPER_LAT_WIDTH = 0.05
@@ -2843,6 +2876,17 @@ def get_kia_xceed_center_taper_scale(desired_lateral_accel: float, v_ego: float)
return 1.0 - reduction
def get_kia_niro_ev_friction_threshold(v_ego: float, desired_lateral_accel: float = 0.0,
desired_lateral_jerk: float = 0.0,
future_lateral_accel: float | None = None) -> float:
speed_weight = np.interp(v_ego, KIA_NIRO_EV_CENTER_FRICTION_SPEED_BP, [0.0, 1.0])
requested_accel = max(abs(desired_lateral_accel), abs(future_lateral_accel or 0.0))
center_weight = np.interp(requested_accel, KIA_NIRO_EV_CENTER_FRICTION_LAT_BP, [1.0, 0.0])
calm_weight = np.interp(abs(desired_lateral_jerk), KIA_NIRO_EV_CENTER_FRICTION_JERK_BP, [1.0, 0.0])
gain = KIA_NIRO_EV_CENTER_FRICTION_THRESHOLD_GAIN * speed_weight * center_weight * calm_weight
return float(get_standard_friction_threshold(v_ego) * (1.0 + gain))
def get_kia_niro_phev_2022_center_taper_scale(desired_lateral_accel: float, v_ego: float) -> float:
speed_weight = _sigmoid((v_ego - KIA_NIRO_PHEV_2022_CENTER_TAPER_SPEED) / KIA_NIRO_PHEV_2022_CENTER_TAPER_SPEED_WIDTH)
center_weight = _sigmoid((KIA_NIRO_PHEV_2022_CENTER_TAPER_LAT - abs(desired_lateral_accel)) / KIA_NIRO_PHEV_2022_CENTER_TAPER_LAT_WIDTH)
@@ -2872,10 +2916,17 @@ def get_kia_stinger_2022_center_taper_scale(desired_lateral_accel: float, v_ego:
def get_kia_stinger_2022_friction_threshold(v_ego: float, desired_lateral_accel: float = 0.0,
desired_lateral_jerk: float = 0.0) -> float:
del desired_lateral_jerk
desired_lateral_jerk: float = 0.0,
future_lateral_accel: float = 0.0, steering_pressed: bool = False) -> float:
speed_weight, center_weight = _kia_stinger_2022_center_weights(desired_lateral_accel, v_ego)
return get_standard_friction_threshold(v_ego) * (1.0 + KIA_STINGER_2022_FRICTION_THRESHOLD_GAIN * speed_weight * center_weight)
threshold = get_standard_friction_threshold(v_ego) * (1.0 + KIA_STINGER_2022_FRICTION_THRESHOLD_GAIN * speed_weight * center_weight)
if not steering_pressed:
highway_weight = np.interp(v_ego, KIA_STINGER_2022_HIGHWAY_FRICTION_SPEED_BP, [0.0, 1.0])
requested_accel = max(abs(desired_lateral_accel), abs(future_lateral_accel))
highway_weight *= np.interp(requested_accel, KIA_STINGER_2022_HIGHWAY_FRICTION_LAT_BP, [1.0, 0.0])
highway_weight *= np.interp(abs(desired_lateral_jerk), KIA_STINGER_2022_HIGHWAY_FRICTION_JERK_BP, [1.0, 0.0])
threshold += KIA_STINGER_2022_HIGHWAY_FRICTION_INCREMENT * highway_weight
return float(threshold)
def _kia_carnival_center_weights(desired_lateral_accel: float, v_ego: float) -> tuple[float, float]:
@@ -2911,6 +2962,15 @@ def get_kia_carnival_friction_threshold(v_ego: float, desired_lateral_accel: flo
return get_hkg_canfd_base_friction_threshold(v_ego) * (1.0 + gain)
def get_kia_carnival_hev_center_weight(v_ego: float, setpoint: float, measurement: float, jerk: float,
future_lateral_accel: float = 0.0) -> float:
speed_weight = np.interp(v_ego, KIA_CARNIVAL_HEV_CENTER_SPEED_BP, [0.0, 1.0, 1.0, 0.0])
center_weight = np.interp(max(abs(setpoint), abs(measurement), abs(future_lateral_accel)), KIA_CARNIVAL_HEV_CENTER_LAT_BP, [1.0, 0.0])
tracking_weight = np.interp(abs(setpoint - measurement), KIA_CARNIVAL_HEV_CENTER_ERROR_BP, [1.0, 0.0])
calm_weight = np.interp(abs(jerk), KIA_CARNIVAL_HEV_CENTER_JERK_BP, [1.0, 0.0])
return float(speed_weight * center_weight * tracking_weight * calm_weight)
def get_kia_carnival_friction_center_fade_scale(desired_lateral_accel: float, v_ego: float) -> float:
speed_weight, center_weight = _kia_carnival_center_weights(desired_lateral_accel, v_ego)
highway_speed_weight, highway_center_weight = _kia_carnival_highway_center_weights(desired_lateral_accel, v_ego)
@@ -2966,7 +3026,10 @@ def get_kia_carnival_unwind_ff_scale(setpoint: float, measured_lateral_accel: fl
def get_kia_carnival_unwind_output_scale(setpoint: float, measured_lateral_accel: float,
desired_lateral_jerk: float, v_ego: float) -> float:
desired_lateral_jerk: float, v_ego: float,
*, output_torque: float | None = None) -> float:
if output_torque is not None and output_torque * setpoint <= 0.0:
return 1.0
if (setpoint * desired_lateral_jerk >= 0.0 or
setpoint * measured_lateral_accel <= 0.0):
return 1.0
@@ -3251,6 +3314,13 @@ def get_genesis_gv70_center_output_scale(desired_lateral_accel: float, v_ego: fl
return 1.0 - (GENESIS_GV70_CENTER_OUTPUT_TAPER_MAX * speed_weight * center_weight)
def _genesis_gv70_curve_damping_weight(setpoint: float, measured_lateral_accel: float) -> float:
curve_weight = float(np.interp(abs(setpoint), GENESIS_GV70_CURVE_DAMPING_LAT_BP, [0.0, 1.0]))
overshoot = max(abs(measured_lateral_accel) - abs(setpoint), 0.0) if setpoint * measured_lateral_accel > 0.0 else 0.0
overshoot_weight = float(np.interp(overshoot, GENESIS_GV70_CURVE_DAMPING_OVERSHOOT_BP, [0.0, 1.0]))
return 1.0 - curve_weight * (1.0 - overshoot_weight)
def get_genesis_gv70_unwind_ff_scale(setpoint: float, measured_lateral_accel: float,
desired_lateral_jerk: float, v_ego: float) -> float:
"""Remove old-turn feedforward when the GV70 has already over-rotated."""
@@ -3264,7 +3334,8 @@ def get_genesis_gv70_unwind_ff_scale(setpoint: float, measured_lateral_accel: fl
GENESIS_GV70_UNWIND_FF_JERK_WIDTH)
speed_weight = _sigmoid((v_ego - GENESIS_GV70_UNWIND_FF_SPEED) /
GENESIS_GV70_UNWIND_FF_SPEED_WIDTH)
return 1.0 - GENESIS_GV70_UNWIND_FF_REDUCTION_MAX * overshoot_weight * jerk_weight * speed_weight
tracking_weight = _genesis_gv70_curve_damping_weight(setpoint, measured_lateral_accel)
return 1.0 - GENESIS_GV70_UNWIND_FF_REDUCTION_MAX * overshoot_weight * jerk_weight * speed_weight * tracking_weight
def get_genesis_gv70_high_speed_error_scale(setpoint: float, measured_lateral_accel: float,
@@ -3281,7 +3352,7 @@ def get_genesis_gv70_high_speed_error_scale(setpoint: float, measured_lateral_ac
phase_weight = 1.0 if setpoint * desired_lateral_jerk < 0.0 else 0.45
reduction = (GENESIS_GV70_HIGH_SPEED_ERROR_DAMPING_MAX * speed_weight * error_weight *
(0.35 + (0.65 * jerk_weight)) * phase_weight)
return 1.0 - reduction
return 1.0 - reduction * _genesis_gv70_curve_damping_weight(setpoint, measured_lateral_accel)
def get_genesis_gv70_reversal_output_scale(setpoint: float, measured_lateral_accel: float,
@@ -3299,7 +3370,7 @@ def get_genesis_gv70_reversal_output_scale(setpoint: float, measured_lateral_acc
jerk_weight = _sigmoid((abs(desired_lateral_jerk) - GENESIS_GV70_REVERSAL_OUTPUT_DAMPING_JERK) /
GENESIS_GV70_REVERSAL_OUTPUT_DAMPING_JERK_WIDTH)
reduction = (GENESIS_GV70_REVERSAL_OUTPUT_DAMPING_MAX * speed_weight * error_weight * jerk_weight)
return 1.0 - reduction
return 1.0 - reduction * _genesis_gv70_curve_damping_weight(setpoint, measured_lateral_accel)
def get_genesis_gv70_low_speed_center_overshoot_scale(setpoint: float, measured_lateral_accel: float,
@@ -3447,6 +3518,19 @@ def get_genesis_g70_friction_threshold(v_ego: float, desired_lateral_accel: floa
return base_threshold * (1.0 + gain)
def get_genesis_g70_traffic_friction_increment(v_ego: float, desired_lateral_accel: float,
desired_lateral_jerk: float, future_curvature: float,
measured_curvature: float) -> float:
speed = max(v_ego, 0.0)
speed_weight = np.interp(speed, GENESIS_G70_TRAFFIC_FRICTION_SPEED_BP, [1.0, 0.0])
request_accel = max(abs(desired_lateral_accel), abs(future_curvature) * speed ** 2)
accel_weight = np.interp(request_accel, GENESIS_G70_TRAFFIC_FRICTION_LAT_BP, [1.0, 0.0])
curvature = max(abs(future_curvature), abs(measured_curvature), abs(desired_lateral_accel) / max(speed ** 2, 1.0))
curve_weight = np.interp(curvature, GENESIS_G70_TRAFFIC_FRICTION_CURVATURE_BP, [1.0, 0.0])
calm_weight = np.interp(abs(desired_lateral_jerk), GENESIS_G70_TRAFFIC_FRICTION_JERK_BP, [1.0, 0.0])
return float(GENESIS_G70_TRAFFIC_FRICTION_INCREMENT * speed_weight * accel_weight * curve_weight * calm_weight)
def get_genesis_g70_friction_jerk_deadzone(v_ego: float, desired_lateral_accel: float,
desired_lateral_jerk: float = 0.0,
measured_lateral_accel: float = 0.0) -> float:
@@ -3725,7 +3809,8 @@ def get_ioniq_5_center_taper_scale(desired_lateral_accel: float, v_ego: float) -
def get_ioniq_5_low_speed_output_limit(desired_lateral_accel: float,
desired_lateral_jerk: float, v_ego: float) -> float:
desired_lateral_jerk: float, v_ego: float,
desired_curvature: float = 0.0, output_torque: float = 0.0) -> float:
"""Bound stop-transition center chatter without blunting actual turns."""
speed_weight = _ioniq_5_sigmoid((IONIQ_5_LOW_SPEED_OUTPUT_LIMIT_SPEED - max(v_ego, 0.0)) /
IONIQ_5_LOW_SPEED_OUTPUT_LIMIT_SPEED_WIDTH)
@@ -3737,6 +3822,9 @@ def get_ioniq_5_low_speed_output_limit(desired_lateral_accel: float,
center_limit = (IONIQ_5_LOW_SPEED_OUTPUT_LIMIT_BASE +
IONIQ_5_LOW_SPEED_OUTPUT_LIMIT_TURN_RELIEF * (1.0 - center_weight))
limit = 1.0 - speed_weight * (1.0 - center_limit)
if desired_curvature * output_torque > 0.0:
turn_weight = np.interp(abs(desired_curvature), IONIQ_5_LOW_SPEED_TURN_CURVATURE_BP, [0.0, 1.0])
limit += max(IONIQ_5_LOW_SPEED_TURN_OUTPUT_FLOOR - limit, 0.0) * turn_weight
return float(np.clip(limit, IONIQ_5_LOW_SPEED_OUTPUT_LIMIT_BASE, 1.0))
@@ -3908,6 +3996,17 @@ def get_ioniq_6_2023_unwind_ff_scale(setpoint: float, measured_lateral_accel: fl
return 1.0 - reduction
def get_ioniq_6_2023_center_friction_increment(v_ego: float, desired_lateral_accel: float, measured_lateral_accel: float,
desired_lateral_jerk: float, future_lateral_accel: float) -> float:
speed_weight = np.interp(v_ego, IONIQ_6_2023_CENTER_FRICTION_SPEED_BP, [0.0, 1.0])
center_weight = np.interp(max(abs(desired_lateral_accel), abs(measured_lateral_accel), abs(future_lateral_accel)),
IONIQ_6_2023_CENTER_FRICTION_LAT_BP, [1.0, 0.0])
tracking_weight = np.interp(abs(desired_lateral_accel - measured_lateral_accel),
IONIQ_6_2023_CENTER_FRICTION_ERROR_BP, [1.0, 0.0])
jerk_weight = np.interp(abs(desired_lateral_jerk), IONIQ_6_2023_CENTER_FRICTION_JERK_BP, [1.0, 0.0])
return float(IONIQ_6_2023_CENTER_FRICTION_INCREMENT * speed_weight * center_weight * tracking_weight * jerk_weight)
def get_ioniq_6_friction_threshold(v_ego: float, desired_lateral_accel: float = 0.0, desired_lateral_jerk: float = 0.0) -> float:
base_threshold = max(get_hkg_canfd_base_friction_threshold(v_ego), IONIQ_6_BASE_FRICTION_THRESHOLD)
transition_envelope = _ioniq_6_transition_envelope(v_ego, desired_lateral_accel, desired_lateral_jerk)
@@ -4740,7 +4839,7 @@ def get_flm_capabilities(car_fingerprint, brand: str = "", hyundai_canfd: bool =
dedicated_friction = car_fingerprint in (
set(BOLT_2022_2023_CARS) | set(BOLT_2018_2021_CARS) | set(VOLT_STANDARD_CARS) | set(PALISADE_CARS) |
set(PRIUS_CARS) | set(RAV4_PRIME_CARS) | set(SIENNA_4TH_GEN_CARS) | set(IONIQ_5_CARS) | set(IONIQ_6_CARS) | set(KIA_EV6_CARS) | set(KIA_FORTE_CARS) |
set(KIA_NIRO_PHEV_2022_CARS) | set(KIA_CARNIVAL_CARS) | set(GENESIS_G90_CARS) | set(GENESIS_G70_CARS) | set(CAMRY_CARS)
set(KIA_NIRO_EV_CARS) | set(KIA_NIRO_PHEV_2022_CARS) | set(KIA_CARNIVAL_CARS) | set(GENESIS_G90_CARS) | set(GENESIS_G70_CARS) | set(CAMRY_CARS)
)
dedicated_center_taper = car_fingerprint in (
set(PRIUS_CARS) | set(SIENNA_4TH_GEN_CARS) | set(BOLT_CARS) | set(VOLT_STANDARD_CARS) | set(IONIQ_5_CARS) |
+3 -17
View File
@@ -20,6 +20,7 @@ from openpilot.selfdrive.controls.lib.lead_behavior import is_radarless_matched_
from openpilot.selfdrive.controls.lib.lead_follow_policy import apply as apply_follow_policy
from openpilot.selfdrive.controls.lib.lead_follow_policy import is_nonurgent_duplicate_vision_follow
from openpilot.selfdrive.controls.lib.longitudinal_vehicle_tunes import (
get_planner_v_ego,
get_far_follow_output_slew_rates,
get_far_follow_output_slew_min_speed,
is_kia_niro_ev_follow_lead,
@@ -527,22 +528,6 @@ VISION_CLOSE_RELEASE_HOLD_MAX_BRAKE = 0.40
MANUAL_STOP_RESUME_OVERRIDE_TIME = 6.0
MANUAL_STOP_RESUME_OVERRIDE_MAX_SPEED = 2.0
def get_planner_v_ego(CP, car_state):
v_ego = max(car_state.vEgo, car_state.vEgoCluster)
is_gm = getattr(CP, "carName", "") == "gm" or getattr(CP, "brand", "") == "gm"
if is_gm and getattr(CP, "enableGasInterceptorDEPRECATED", False):
try:
from opendbc.car.gm.values import GMFlags
is_gm_pedal_long = bool(CP.flags & GMFlags.PEDAL_LONG.value)
if is_gm_pedal_long:
return float(car_state.vEgo)
except Exception:
pass
return float(v_ego)
def get_accel_from_plan_classic(CP, speeds, accels, vEgoStopping):
if len(speeds) == CONTROL_N:
v_target_now = np.interp(DT_MDL, CONTROL_N_T_IDX, speeds)
@@ -2145,7 +2130,8 @@ class LongitudinalPlanner:
not bool(getattr(sm['starpilotPlan'], 'forcingStop', False)) and
not bool(getattr(sm['starpilotPlan'], 'redLight', False)) and
not bool(getattr(sm['starpilotPlan'], 'stopSignConfirmed', False)) and
any(is_kia_niro_ev_follow_lead(self.CP, lead, scene_v_ego) for lead in (self.lead_one, self.lead_two))
any(is_kia_niro_ev_follow_lead(self.CP, lead, scene_v_ego, sm['modelV2'].position)
for lead in (self.lead_one, self.lead_two))
)
lightning_stopped_radar_follow = (
experimental_mode and
@@ -144,6 +144,26 @@ FORD_MACH_E_FORCE_STOP_LOW_SPEED_HOLD = 1.0
KIA_CARNIVAL_2025_STOP_SIGN_LOW_SPEED_HOLD = 0.75
def get_planner_v_ego(CP, car_state):
v_ego = max(car_state.vEgo, car_state.vEgoCluster)
if (getattr(CP, "carFingerprint", None) == "KIA_RAY_EV" and
getattr(CP, "enableGasInterceptorDEPRECATED", False)):
return float(car_state.vEgo)
is_gm = getattr(CP, "carName", "") == "gm" or getattr(CP, "brand", "") == "gm"
if is_gm and getattr(CP, "enableGasInterceptorDEPRECATED", False):
try:
from opendbc.car.gm.values import GMFlags
is_gm_pedal_long = bool(CP.flags & GMFlags.PEDAL_LONG.value)
if is_gm_pedal_long:
return float(car_state.vEgo)
except Exception:
pass
return float(v_ego)
def get_toyota_prius_stopped_lead_obstacle_bias(CP, lead, v_ego):
"""Move the ordinary Prius stopped-lead target back without touching stop targets."""
if (
@@ -589,15 +609,32 @@ def allow_radar_standstill_gap_settle(CP):
)
def is_kia_niro_ev_follow_lead(CP, lead, v_ego):
def is_kia_niro_ev_follow_lead(CP, lead, v_ego, model_position=None):
if (
getattr(CP, "brand", "") != "hyundai" or str(getattr(CP, "carFingerprint", "")) != "KIA_NIRO_EV" or
lead is None or not bool(getattr(lead, "status", False)) or bool(getattr(lead, "radar", False)) or
float(getattr(lead, "modelProb", 0.0)) < 0.95 or
abs(float(getattr(lead, "yRel", 0.0))) > 1.5 or float(v_ego) < 5.0
not float(getattr(lead, "modelProb", 0.0)) >= 0.95 or float(v_ego) < 5.0
):
return False
return 10.0 <= float(lead.dRel) <= max(40.0, 2.5 * float(v_ego))
distance = float(lead.dRel)
lateral_offset = float(getattr(lead, "yRel", 0.0))
if abs(lateral_offset) <= 1.5 and 10.0 <= distance <= max(40.0, 2.5 * float(v_ego)):
return True
if not (
model_position is not None and
10.0 <= distance <= max(40.0, 3.5 * float(v_ego)) and
float(v_ego) - float(getattr(lead, "vLead", v_ego)) >= 0.75 and
float(getattr(lead, "aLeadK", 0.0)) <= -0.15
):
return False
path_x = np.asarray(getattr(model_position, "x", ()), dtype=float)
path_y = np.asarray(getattr(model_position, "y", ()), dtype=float)
if (len(path_x) < 2 or len(path_x) != len(path_y) or distance > path_x[-1] or
not np.all(np.isfinite(path_x)) or not np.all(np.isfinite(path_y)) or not np.all(np.diff(path_x) > 0.0)):
return False
return abs(lateral_offset + float(np.interp(distance, path_x, path_y))) <= 1.5
def get_far_follow_output_slew_min_speed(CP, default_min_speed):
+180 -11
View File
@@ -814,7 +814,7 @@ class TestLatControl:
assert get_genesis_gv70_unwind_ff_scale(-0.3, -0.3, -0.8, 15.0) == 1.0
early_unwind = get_genesis_gv70_unwind_ff_scale(-0.7, -0.6, 0.8, 15.0)
assert early_unwind < 1.0
assert early_unwind == 1.0
reduced = get_genesis_gv70_unwind_ff_scale(-0.2, -1.0, 1.0, 20.0)
assert 0.6 < reduced < 1.0
assert get_genesis_gv70_unwind_ff_scale(-0.2, -1.0, -1.0, 20.0) == 1.0
@@ -958,19 +958,59 @@ class TestLatControl:
def test_genesis_gv70_high_speed_error_damping(self):
assert get_genesis_gv70_high_speed_error_scale(0.2, 0.2, 0.8, 20.0) == 1.0
assert get_genesis_gv70_high_speed_error_scale(-0.7, 0.58, -0.8, 33.5) < 1.0
assert get_genesis_gv70_high_speed_error_scale(-0.7, 0.58, -0.8, 20.0) > \
get_genesis_gv70_high_speed_error_scale(-0.7, 0.58, -0.8, 33.5)
assert get_genesis_gv70_high_speed_error_scale(-0.7, 0.58, -0.8, 33.5) == 1.0
assert get_genesis_gv70_high_speed_error_scale(-0.7, -0.95, 0.8, 20.0) > \
get_genesis_gv70_high_speed_error_scale(-0.7, -0.95, 0.8, 33.5)
def test_genesis_gv70_reversal_damping_is_medium_speed_and_phase_gated(self):
same_direction = get_genesis_gv70_reversal_output_scale(0.7, 0.9, 0.8, 16.0)
low_speed = get_genesis_gv70_reversal_output_scale(-0.7, 0.7, -0.8, 8.0)
route_speed = get_genesis_gv70_reversal_output_scale(-0.7, 0.7, -0.8, 15.0)
low_speed = get_genesis_gv70_reversal_output_scale(-0.7, -1.0, 0.8, 8.0)
route_speed = get_genesis_gv70_reversal_output_scale(-0.7, -1.0, 0.8, 15.0)
assert same_direction == pytest.approx(1.0)
assert route_speed < 1.0
assert route_speed < low_speed
@pytest.mark.parametrize("direction", [-1.0, 1.0])
def test_genesis_gv70_curve_damping_preserves_tracking_and_direction_changes(self, direction):
for scale in (get_genesis_gv70_unwind_ff_scale, get_genesis_gv70_high_speed_error_scale,
get_genesis_gv70_reversal_output_scale):
for measured in (0.0, 0.55, 0.8, -0.8):
for jerk in (-0.8, 0.8):
assert scale(direction * 0.8, direction * measured, direction * jerk, 30.0) == 1.0
assert scale(direction * 0.8, direction * 1.1, -direction * 0.8, 30.0) < 1.0
def test_genesis_gv70_curve_damping_is_continuous_and_leaves_center_unchanged(self):
weight = latcontrol_vehicle_tunes._genesis_gv70_curve_damping_weight
for setpoint in (-0.25, 0.0, 0.25):
for measured in (-1.0, 0.0, 1.0):
assert weight(setpoint, measured) == 1.0
for setpoint in (-0.8, -0.55, -0.25, 0.25, 0.55, 0.8):
for measured in (setpoint - 0.2, setpoint, setpoint + 0.2):
assert 0.0 <= weight(setpoint, measured) <= 1.0
assert abs(weight(setpoint - 1e-7, measured) - weight(setpoint + 1e-7, measured)) < 2e-6
assert abs(weight(setpoint, measured - 1e-7) - weight(setpoint, measured + 1e-7)) < 2e-6
@pytest.mark.parametrize("direction", [-1.0, 1.0])
def test_genesis_gv70_undertracking_curve_keeps_undamped_feedforward(self, monkeypatch, direction):
def trace():
controller, VM, CS, params, toggles = self._build_torque_controller(HYUNDAI.GENESIS_GV70_ELECTRIFIED_1ST_GEN)
CS.vEgo = 30.0
CS.steeringAngleDeg = math.degrees(VM.get_steer_from_curvature(-direction * 0.6 / CS.vEgo ** 2, CS.vEgo, 0.0))
request = direction * 0.8 / CS.vEgo ** 2
controller.curvature_request_buffer = deque([request] * controller.request_buffer_len, maxlen=controller.request_buffer_len)
rows = []
for _ in range(100):
output, _, state = controller.update(True, CS, VM, params, False, request, False, 0.2, None, None, toggles)
rows.append((output, state.desiredLateralAccel, state.p, state.i, state.d, state.f))
return rows
actual = trace()
for name in ("get_genesis_gv70_unwind_ff_scale", "get_genesis_gv70_high_speed_error_scale",
"get_genesis_gv70_reversal_output_scale"):
monkeypatch.setattr(latcontrol_torque, name, lambda *_args: 1.0)
assert actual == trace()
def test_genesis_gv70_low_speed_center_overshoot_damping(self):
center_overshoot = get_genesis_gv70_low_speed_center_overshoot_scale(0.02, 0.45, 22.0 * 0.44704)
clean_center = get_genesis_gv70_low_speed_center_overshoot_scale(0.02, 0.02, 22.0 * 0.44704)
@@ -1529,14 +1569,17 @@ class TestLatControl:
(60.0, 0.0, 0.0),
(100.0, 0.0, 0.0),
(100.0, 0.65, 0.0),
(110.0, 0.0, 0.15),
(110.0, 0.65, 0.15),
(105.0, 0.0, 0.15),
(110.0, 0.0, 0.30),
(110.0, 0.65, 0.30),
(120.0, 0.0, 0.30),
(120.0, 0.65, 0.30),
(120.0, -0.65, 0.30),
(120.0, 1.15, 0.15),
(120.0, -1.15, 0.15),
(120.0, 1.5, 0.0),
(120.0, 1.15, 0.30),
(120.0, -1.15, 0.30),
(120.0, 1.6, 0.15),
(120.0, -1.6, 0.15),
(120.0, 2.0, 0.0),
(140.0, 0.0, 0.30),
(140.0, -2.0, 0.0),
])
@@ -1614,6 +1657,30 @@ class TestLatControl:
assert turn > center
assert highway > center
@pytest.mark.parametrize("direction", [-1, 1])
def test_ioniq_5_creep_turn_relief_preserves_center_and_unwind(self, direction):
args = (direction * 0.03, direction * 0.05, 1.0)
legacy = get_ioniq_5_low_speed_output_limit(*args)
assert get_ioniq_5_low_speed_output_limit(*args, direction * 0.02, direction) == pytest.approx(0.30)
for curvature, torque in [(0.0, direction), (direction * 0.005, direction),
(direction * 0.02, -direction), (direction * 0.02, 0.0)]:
assert get_ioniq_5_low_speed_output_limit(*args, curvature, torque) == legacy
for speed in (10.0, 15.0, 25.0, 35.0):
assert get_ioniq_5_low_speed_output_limit(*args[:2], speed, direction * 0.1, direction) == \
get_ioniq_5_low_speed_output_limit(*args[:2], speed)
def test_ioniq_5_creep_turn_relief_is_continuous_and_bounded(self):
args = (0.03, 0.05, 1.0)
legacy = get_ioniq_5_low_speed_output_limit(*args)
for step in range(101):
curvature = step * 0.0004
limit = get_ioniq_5_low_speed_output_limit(*args, curvature, 1.0)
assert legacy <= limit <= 0.30
assert limit == pytest.approx(get_ioniq_5_low_speed_output_limit(-args[0], -args[1], args[2], -curvature, -1.0))
for boundary in (0.005, 0.020):
assert abs(get_ioniq_5_low_speed_output_limit(*args, boundary + 1e-7, 1.0) -
get_ioniq_5_low_speed_output_limit(*args, boundary - 1e-7, 1.0)) < 1e-5
def test_ioniq_ev_old_ff_scale_curve(self):
assert get_ioniq_ev_old_ff_scale(0.0, 0.0, 20.0) == 1.0
assert get_ioniq_ev_old_ff_scale(0.35, 0.0, 20.0) > get_ioniq_ev_old_ff_scale(-0.35, 0.0, 20.0)
@@ -2092,6 +2159,38 @@ class TestLatControl:
assert lac_log.active
assert abs(output) <= 0.05
@pytest.mark.parametrize("direction", [-1, 1])
@pytest.mark.parametrize("driver_state", ["clear", "pressed", "released"])
def test_ioniq_5_creep_turn_relief_update_path(self, monkeypatch, direction, driver_state):
controller, VM, CS, params, toggles = self._build_torque_controller(HYUNDAI.HYUNDAI_IONIQ_5)
CS.vEgo = 1.0
CS.steeringPressed = driver_state == "pressed"
controller.prev_steering_pressed = driver_state == "released"
curvature = direction * 0.04
controller.curvature_request_buffer = deque([curvature] * controller.request_buffer_len,
maxlen=controller.request_buffer_len)
limit = latcontrol_torque.get_ioniq_5_low_speed_output_limit
baseline, _, old_log = controller.update(True, CS, VM, params, False, curvature, False, 0.2, None, None, toggles)
controller.pid.reset()
controller.prev_steering_pressed = driver_state == "released"
monkeypatch.setattr(latcontrol_torque, "get_ioniq_5_low_speed_output_limit", lambda accel, jerk, speed, *_args: limit(accel, jerk, speed))
original, _, new_log = controller.update(True, CS, VM, params, False, curvature, False, 0.2, None, None, toggles)
assert old_log.desiredLateralAccel == new_log.desiredLateralAccel
assert old_log.actualLateralAccel == new_log.actualLateralAccel
if driver_state != "clear":
assert baseline == original
else:
assert abs(original) < abs(baseline) <= 0.30
assert baseline * direction < 0.0
@pytest.mark.parametrize("car_name", [HYUNDAI.HYUNDAI_IONIQ_6, HYUNDAI.HYUNDAI_IONIQ_5_PE, HYUNDAI.KIA_EV6])
def test_ioniq_5_creep_turn_relief_does_not_change_other_cars(self, monkeypatch, car_name):
monkeypatch.setattr(latcontrol_torque, "get_ioniq_5_low_speed_output_limit", lambda *_args: pytest.fail("Ioniq 5 limit reached another car"))
controller, VM, CS, params, toggles = self._build_torque_controller(car_name, force_torque=True)
CS.vEgo = 1.0
controller.update(True, CS, VM, params, False, 0.04, False, 0.2, None, None, toggles)
assert not controller.is_ioniq_5
def test_ioniq_6_default_update_path(self):
controller, VM, CS, params, starpilot_toggles = self._build_torque_controller(HYUNDAI.HYUNDAI_IONIQ_6)
@@ -2159,6 +2258,76 @@ class TestLatControl:
assert center_threshold == pytest.approx(base_threshold * 1.10, rel=0.01)
assert turn_threshold == pytest.approx(base_threshold, rel=0.01)
@pytest.mark.parametrize("direction", [-1, 1])
def test_kia_stinger_2022_highway_friction_preserves_turns_and_low_speed(self, direction):
threshold = get_kia_stinger_2022_friction_threshold
for speed in (0.0, 10.0, 20.0, 50.0 * 0.44704):
assert threshold(speed, direction * 0.10, direction * 0.10) == threshold(speed, direction * 0.10, direction * 0.10, 0.0, True)
for desired, future, jerk in [(0.40, 0.0, 0.0), (0.05, 0.40, 0.0), (0.0, 0.0, 0.75)]:
args = (30.0, direction * desired, direction * jerk, direction * future)
assert threshold(*args) == threshold(*args, True)
args = (30.0, direction * 0.05, direction * 0.10)
assert threshold(*args) == pytest.approx(threshold(*args, 0.0, True) + 0.20)
def test_kia_stinger_2022_highway_friction_is_symmetric_continuous_and_bounded(self):
threshold = get_kia_stinger_2022_friction_threshold
for step in range(101):
speed, desired, jerk = step * 0.4, step * 0.006, step * 0.01
value = threshold(speed, desired, jerk, desired)
base = threshold(speed, desired, jerk, desired, True)
assert base <= value <= base + 0.20
assert value == threshold(speed, -desired, -jerk, -desired)
for speed in (50.0 * 0.44704, 60.0 * 0.44704):
assert abs(threshold(speed + 1e-7) - threshold(speed - 1e-7)) < 1e-5
for desired in (0.15, 0.40):
assert abs(threshold(30.0, desired + 1e-7) - threshold(30.0, desired - 1e-7)) < 1e-5
for jerk in (0.20, 0.75):
assert abs(threshold(30.0, 0.0, jerk + 1e-7) - threshold(30.0, 0.0, jerk - 1e-7)) < 1e-5
def test_kia_stinger_2022_highway_friction_retains_full_compensation(self):
tune = car.CarParams.LateralTorqueTuning.new_message(friction=0.105, latAccelFactor=2.82)
quiet = get_kia_stinger_2022_friction_threshold(30.0)
base = get_kia_stinger_2022_friction_threshold(30.0, 0.0, 0.0, 0.0, True)
for error in (-1.0, -0.10, 0.10, 1.0):
new = get_friction(error, 0.0, quiet, tune)
old = get_friction(error, 0.0, base, tune)
if abs(error) >= 1.0:
assert new == old
else:
assert abs(new) < abs(old)
@pytest.mark.parametrize("driver_state", ["clear", "pressed", "released"])
def test_kia_stinger_2022_highway_friction_update_path(self, monkeypatch, driver_state):
threshold = latcontrol_torque.get_kia_stinger_2022_friction_threshold
def output():
controller, VM, CS, params, toggles = self._build_torque_controller(HYUNDAI.KIA_STINGER_2022)
CS.vEgo = 60.0 * 0.44704
CS.steeringAngleDeg = 0.5
CS.steeringPressed = driver_state == "pressed"
controller.prev_steering_pressed = driver_state == "released"
return controller.update(True, CS, VM, params, False, 0.0, False, 0.2, None, None, toggles)
quiet, _, quiet_log = output()
monkeypatch.setattr(latcontrol_torque, "get_kia_stinger_2022_friction_threshold",
lambda speed, desired, jerk, future, _pressed: threshold(speed, desired, jerk, future, True))
baseline, _, original_log = output()
assert quiet_log.desiredLateralAccel == original_log.desiredLateralAccel
assert quiet_log.actualLateralAccel == original_log.actualLateralAccel
assert quiet_log.p == original_log.p
assert quiet_log.i == original_log.i
if driver_state == "clear":
assert abs(quiet) < abs(baseline)
else:
assert quiet == baseline
@pytest.mark.parametrize("car_name", [HYUNDAI.KIA_STINGER, HYUNDAI.GENESIS_G70_2020, HYUNDAI.HYUNDAI_IONIQ_5])
def test_kia_stinger_2022_highway_friction_does_not_change_other_cars(self, monkeypatch, car_name):
monkeypatch.setattr(latcontrol_torque, "get_kia_stinger_2022_friction_threshold",
lambda *_args: pytest.fail("Stinger 2022 threshold reached another car"))
controller, VM, CS, params, toggles = self._build_torque_controller(car_name, force_torque=True)
CS.vEgo = 60.0 * 0.44704
controller.update(True, CS, VM, params, False, 0.0, False, 0.2, None, None, toggles)
assert not controller.is_kia_stinger_2022
def test_kia_stinger_2022_default_update_path(self):
controller, VM, CS, params, starpilot_toggles = self._build_torque_controller(HYUNDAI.KIA_STINGER_2022)
CS.vEgo = 20.0

Some files were not shown because too many files have changed in this diff Show More