mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-01 03:24:05 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 98f8443122 | |||
| ad2bb3a212 | |||
| a25611435c | |||
| 823a5d5674 | |||
| 1391d3a39c | |||
| 807acc0522 | |||
| b220a37f4b | |||
| 3ccae7b88d | |||
| 88c73987d0 | |||
| 83f914fc5c |
@@ -1,26 +1,19 @@
|
||||
# StarPilot
|
||||
|
||||
[](https://deepwiki.com/firestar5683/StarPilot)
|
||||
[](https://deepwiki.com/StarPilotHQ/StarPilot)
|
||||
[](https://firestar.link/discord)
|
||||
[](https://github.com/firestar5683/StarPilot)
|
||||
[](https://github.com/StarPilotHQ/StarPilot)
|
||||
[](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,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.
@@ -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.
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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" ;
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,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.
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 @@
|
||||
DEV-956aceaf-DEBUG
|
||||
DEV-b220a37f-DEBUG
|
||||
@@ -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) |
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user