Compare commits

..

14 Commits

Author SHA1 Message Date
firestarsdog 2efe6017bf polish 2026-10-02 22:12:32 -04:00
firestar5683 3293a34219 build 2026-10-02 21:04:12 -05:00
firestar5683 d2cd3fd0ed Quintessence: A Quest 2026-10-02 21:03:49 -05:00
firestarsdog 4ac874f6b4 polish 2026-10-02 20:30:33 -04:00
firestarsdog 1aefe9dc82 more font size polish 2026-10-02 20:03:46 -04:00
firestarsdog 0857acfc50 big ui text size polish 2026-10-02 14:24:05 -04:00
firestar5683 5c3ddd58e1 build 2026-10-01 21:09:41 -05:00
firestar5683 7848a0090a Soup 2026-10-01 21:08:43 -05:00
firestar5683 86599a27bc Flood Warning 2026-10-01 14:11:28 -05:00
Dom d9f58acbb7 Merge pull request #194 from N30-PH/starpilot-honda-city-2025-firmware
Honda: add observed 2025 City firmware versions
2026-10-01 12:43:20 -05:00
firestar5683 42d4a5b207 Jalisco's 2026-09-30 17:16:41 -05:00
N30 b22e2d678a Honda City: document Brazilian model years 2023-25
Follow sunnypilot/opendbc#487 and commaai/opendbc#3812.
Only update the existing documentation label; vehicle parameters are unchanged.
2026-09-30 16:13:34 -03:00
N30 26433e078e Honda: add observed 2025 City firmware versions
Add six firmware versions from the owner's recorded City EXL 2025 inventory.
Preserve existing firmware and control settings. Credit baninfelipe and
sunnypilot/opendbc#487; related upstream contribution commaai/opendbc#3812.
2026-09-30 16:13:21 -03:00
firestar5683 bf1b916d50 Aldi 2026-09-29 20:22:20 -05:00
133 changed files with 3403 additions and 2632 deletions
-2
View File
@@ -237,8 +237,6 @@ struct StarPilotPlan @0xf98d843bfd7004a3 {
cscLearnedLatAccel @40 :Float32; # learned comfort at the current curvature, before margin cscLearnedLatAccel @40 :Float32; # learned comfort at the current curvature, before margin
cscBindingDistance @41 :Float32; # distance to the horizon point setting the target, m cscBindingDistance @41 :Float32; # distance to the horizon point setting the target, m
approachStopLength @42 :Float32; # pre-commit distance to a detected stop, m; 0 when off approachStopLength @42 :Float32; # pre-commit distance to a detected stop, m; 0 when off
slcPresentedSpeedLimitSource @43 :Text; # source of the shown accepted or pending posted limit
slcIsLimitingMaxSet @44 :Bool; # SLC target is below the configured Max Set
} }
struct StarPilotRadarState @0xb86e6369214c01c8 { struct StarPilotRadarState @0xb86e6369214c01c8 {
Binary file not shown.
+1
View File
@@ -728,6 +728,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"SteerRatio", {PERSISTENT, FLOAT, "0.0", "0.0", 3}}, {"SteerRatio", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"SteerRatioStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}}, {"SteerRatioStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
{"EnableTorqueBarWidget", {PERSISTENT, BOOL, "1", "0", 0}}, {"EnableTorqueBarWidget", {PERSISTENT, BOOL, "1", "0", 0}},
{"StingerObjectShadow", {PERSISTENT, BOOL, "0", "0", 3}},
{"StockConfidenceBallWidget", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}}, {"StockConfidenceBallWidget", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
{"StockDongleId", {PERSISTENT, STRING, "", ""}}, {"StockDongleId", {PERSISTENT, STRING, "", ""}},
{"StopAccel", {PERSISTENT, FLOAT, "0.0", "0.0", 3}}, {"StopAccel", {PERSISTENT, FLOAT, "0.0", "0.0", 3}},
Binary file not shown.
+1 -1
View File
@@ -264,7 +264,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Kia|Forte 2019-21|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|6 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Forte 2019-21">Buy Here</a></sub></details>||| |Kia|Forte 2019-21|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|6 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Forte 2019-21">Buy Here</a></sub></details>|||
|Kia|Forte 2022-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Forte 2022-23">Buy Here</a></sub></details>||| |Kia|Forte 2022-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Forte 2022-23">Buy Here</a></sub></details>|||
|Kia|K4 (with HDA II) 2025|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai R connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K4 (with HDA II) 2025">Buy Here</a></sub></details>||| |Kia|K4 (with HDA II) 2025|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai R connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K4 (with HDA II) 2025">Buy Here</a></sub></details>|||
|Kia|K4 (without HDA II) 2025|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K4 (without HDA II) 2025">Buy Here</a></sub></details>||| |Kia|K4 (without HDA II) 2025-26|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K4 (without HDA II) 2025-26">Buy Here</a></sub></details>|||
|Kia|K5 2021-24|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K5 2021-24">Buy Here</a></sub></details>||| |Kia|K5 2021-24|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K5 2021-24">Buy Here</a></sub></details>|||
|Kia|K5 (without HDA II) 2025|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai M connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K5 (without HDA II) 2025">Buy Here</a></sub></details>||| |Kia|K5 (without HDA II) 2025|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai M connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K5 (without HDA II) 2025">Buy Here</a></sub></details>|||
|Kia|K5 Hybrid 2020-22|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K5 Hybrid 2020-22">Buy Here</a></sub></details>||| |Kia|K5 Hybrid 2020-22|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K5 Hybrid 2020-22">Buy Here</a></sub></details>|||
+15 -1
View File
@@ -29,6 +29,9 @@ class CarState(CarStateBase):
self.distance_button = 0 self.distance_button = 0
self.lc_button = 0 self.lc_button = 0
self.cancel_button = False
self.cancel_resume_pressed = False
self.cancel_resume_is_cancel = False
self.lkas_available = False self.lkas_available = False
self.lateral_motion_control = None self.lateral_motion_control = None
self.lateral_control_status = None self.lateral_control_status = None
@@ -156,6 +159,14 @@ class CarState(CarStateBase):
prev_lc_button = self.lc_button prev_lc_button = self.lc_button
self.distance_button = cp.vl["Steering_Data_FD1"]["AccButtnGapTogglePress"] self.distance_button = cp.vl["Steering_Data_FD1"]["AccButtnGapTogglePress"]
self.lc_button = bool(cp.vl["Steering_Data_FD1"]["TjaButtnOnOffPress"]) self.lc_button = bool(cp.vl["Steering_Data_FD1"]["TjaButtnOnOffPress"])
prev_cancel_button = self.cancel_button
cancel_resume_pressed = bool(cp.vl["Steering_Data_FD1"]["CcAslButtnCnclResPress"])
if cancel_resume_pressed and not self.cancel_resume_pressed:
self.cancel_resume_is_cancel = ret.cruiseState.available and ret.cruiseState.enabled
self.cancel_resume_pressed = cancel_resume_pressed
self.cancel_button = bool(cp.vl["Steering_Data_FD1"]["CcAslButtnCnclPress"]) or (
cancel_resume_pressed and self.cancel_resume_is_cancel
)
# lock info # lock info
ret.doorOpen = any([cp.vl["BodyInfo_3_FD1"]["DrStatDrv_B_Actl"], cp.vl["BodyInfo_3_FD1"]["DrStatPsngr_B_Actl"], ret.doorOpen = any([cp.vl["BodyInfo_3_FD1"]["DrStatDrv_B_Actl"], cp.vl["BodyInfo_3_FD1"]["DrStatPsngr_B_Actl"],
@@ -183,10 +194,13 @@ class CarState(CarStateBase):
except KeyError: except KeyError:
self.lateral_motion_control = None self.lateral_motion_control = None
ret.buttonEvents = [ button_events = [
*create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise}), *create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise}),
*create_button_events(self.lc_button, prev_lc_button, {1: ButtonType.lkas}), *create_button_events(self.lc_button, prev_lc_button, {1: ButtonType.lkas}),
] ]
if self.CP.openpilotLongitudinalControl:
button_events += create_button_events(self.cancel_button, prev_cancel_button, {1: ButtonType.cancel})
ret.buttonEvents = button_events
fp_ret = custom.StarPilotCarState.new_message() fp_ret = custom.StarPilotCarState.new_message()
fp_ret.brakeLights = ret.brakePressed fp_ret.brakeLights = ret.brakePressed
@@ -10,11 +10,12 @@ from opendbc.car import Bus, gen_empty_fingerprint
from opendbc.can import CANPacker from opendbc.can import CANPacker
from opendbc.car.ford import fordcan from opendbc.car.ford import fordcan
from opendbc.car.ford.carcontroller import FordStockCruiseButton, apply_creep_compensation from opendbc.car.ford.carcontroller import FordStockCruiseButton, apply_creep_compensation
from opendbc.car.ford.carstate import CarState
from opendbc.car.gps import FORD_MACH_E_GPS_MESSAGES, get_car_gps_config, parse_ford_can_gps from opendbc.car.gps import FORD_MACH_E_GPS_MESSAGES, get_car_gps_config, parse_ford_can_gps
from opendbc.car.structs import CarParams from opendbc.car.structs import CarParams, CarState as CarStateStruct
from opendbc.car.fw_versions import build_fw_dict from opendbc.car.fw_versions import build_fw_dict
from opendbc.car.ford.interface import CarInterface from opendbc.car.ford.interface import CarInterface
from opendbc.car.ford.values import CAR, FW_QUERY_CONFIG, FW_PATTERN, FordSafetyFlags, get_platform_codes, match_vin_to_car from opendbc.car.ford.values import CAR, DBC, FW_QUERY_CONFIG, FW_PATTERN, FordSafetyFlags, get_platform_codes, match_vin_to_car
from opendbc.car.ford.fingerprints import FW_VERSIONS from opendbc.car.ford.fingerprints import FW_VERSIONS
Ecu = CarParams.Ecu Ecu = CarParams.Ecu
@@ -38,6 +39,46 @@ def test_stock_cruise_button_ignores_press_with_cruise_master_off():
assert button.update(True, cruise_available=False, cruise_enabled=False) == (False, False) assert button.update(True, cruise_available=False, cruise_enabled=False) == (False, False)
@pytest.mark.parametrize("op_long", (False, True))
@pytest.mark.parametrize("standstill", (False, True))
@pytest.mark.parametrize("cruise_status", (4, 5))
@pytest.mark.parametrize("switch", ("CcAslButtnCnclResPress", "CcAslButtnCnclPress"))
def test_ford_cancel_event_does_not_require_pcm_disengagement(op_long, standstill, cruise_status, switch):
CP = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, gen_empty_fingerprint(), [], op_long, False, False, None)
state = CarState(CP, None)
parsers = state.get_can_parsers(CP)
packer = CANPacker(DBC[CP.carFingerprint][Bus.pt])
for frame, (pressed, status) in enumerate(((True, cruise_status), (True, 3), (False, 3)), start=1):
messages = [
packer.make_can_msg("Steering_Data_FD1", fordcan.CanBus(CP).main, {switch: int(pressed)}),
packer.make_can_msg("EngBrakeData", fordcan.CanBus(CP).main, {"CcStat_D_Actl": status}),
packer.make_can_msg("DesiredTorqBrk", fordcan.CanBus(CP).main, {"VehStop_D_Stat": int(standstill)}),
]
parsers[Bus.pt].update([(frame * 100_000_000, messages)])
ret, _ = state.update(parsers, None)
assert ret.standstill == standstill
expected = [(CarStateStruct.ButtonEvent.Type.cancel, pressed)] if op_long and frame != 2 else []
assert [(event.type, event.pressed) for event in ret.buttonEvents] == expected
@pytest.mark.parametrize("initial_status", (0, 3))
def test_ford_resume_does_not_become_cancel_when_pcm_engages(initial_status):
CP = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, gen_empty_fingerprint(), [], True, False, False, None)
state = CarState(CP, None)
parsers = state.get_can_parsers(CP)
packer = CANPacker(DBC[CP.carFingerprint][Bus.pt])
for frame, (pressed, status) in enumerate(((True, initial_status), (True, 4), (False, 4)), start=1):
messages = [
packer.make_can_msg("Steering_Data_FD1", fordcan.CanBus(CP).main, {"CcAslButtnCnclResPress": int(pressed)}),
packer.make_can_msg("EngBrakeData", fordcan.CanBus(CP).main, {"CcStat_D_Actl": status}),
]
parsers[Bus.pt].update([(frame * 100_000_000, messages)])
ret, _ = state.update(parsers, None)
assert not ret.buttonEvents
def test_mach_e_does_not_apply_engine_creep_compensation(): def test_mach_e_does_not_apply_engine_creep_compensation():
for accel in (-1.0, -0.1, 0.0, 0.1): for accel in (-1.0, -0.1, 0.0, 0.1):
assert apply_creep_compensation(accel, 0.5, CAR.FORD_MUSTANG_MACH_E_MK1, assert apply_creep_compensation(accel, 0.5, CAR.FORD_MUSTANG_MACH_E_MK1,
@@ -1065,21 +1065,27 @@ FW_VERSIONS = {
CAR.HONDA_CITY_7G: { CAR.HONDA_CITY_7G: {
(Ecu.eps, 0x18da30f1, None): [ (Ecu.eps, 0x18da30f1, None): [
b'39990-T14-B030\x00\x00', b'39990-T14-B030\x00\x00',
b'39990-T14-B510\x00\x00',
], ],
(Ecu.gateway, 0x18daeff1, None): [ (Ecu.gateway, 0x18daeff1, None): [
b'38897-T14-M110\x00\x00', b'38897-T14-M110\x00\x00',
b'38897-T14-M210\x00\x00',
], ],
(Ecu.srs, 0x18da53f1, None): [ (Ecu.srs, 0x18da53f1, None): [
b'77959-T00-B830\x00\x00', b'77959-T00-B830\x00\x00',
b'77959-T14-B810\x00\x00',
], ],
(Ecu.fwdRadar, 0x18dab0f1, None): [ (Ecu.fwdRadar, 0x18dab0f1, None): [
b'36161-T14-P050\x00\x00', b'36161-T14-P050\x00\x00',
b'8S102-T14-P020\x00\x00',
], ],
(Ecu.vsa, 0x18da28f1, None): [ (Ecu.vsa, 0x18da28f1, None): [
b'57114-T14-B030\x00\x00', b'57114-T14-B030\x00\x00',
b'57114-T14-M510\x00\x00',
], ],
(Ecu.transmission, 0x18da1ef1, None): [ (Ecu.transmission, 0x18da1ef1, None): [
b'28101-63B-M420\x00\x00', b'28101-63B-M420\x00\x00',
b'28101-63B-M510\x00\x00',
], ],
}, },
CAR.HONDA_PASSPORT_4G: { CAR.HONDA_PASSPORT_4G: {
+1 -1
View File
@@ -277,7 +277,7 @@ class CAR(Platforms):
flags=HondaFlags.BOSCH_RADARLESS, flags=HondaFlags.BOSCH_RADARLESS,
) )
HONDA_CITY_7G = HondaBoschPlatformConfig( HONDA_CITY_7G = HondaBoschPlatformConfig(
[HondaCarDocs("Honda City (Brazil only) 2023", "All")], [HondaCarDocs("Honda City (Brazil only) 2023-25", "All")],
CarSpecs(mass=3125 * CV.LB_TO_KG, wheelbase=2.6, steerRatio=19.0, centerToFrontRatio=0.41, minSteerSpeed=23. * CV.KPH_TO_MS), CarSpecs(mass=3125 * CV.LB_TO_KG, wheelbase=2.6, steerRatio=19.0, centerToFrontRatio=0.41, minSteerSpeed=23. * CV.KPH_TO_MS),
{Bus.pt: 'honda_bosch_radarless_generated'}, {Bus.pt: 'honda_bosch_radarless_generated'},
flags=HondaFlags.BOSCH_RADARLESS, flags=HondaFlags.BOSCH_RADARLESS,
@@ -1043,9 +1043,12 @@ class CarController(CarControllerBase):
left_sound_active=left_warning.sound_active, right_sound_active=right_warning.sound_active, left_sound_active=left_warning.sound_active, right_sound_active=right_warning.sound_active,
) )
else: else:
host_speed = getattr(CS.out, "vEgoRaw", None) \
if self.CP.carFingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN else None
adrv_messages = hyundaicanfd.create_adrv_messages(self.packer, self.CAN, self.frame, adrv_messages = hyundaicanfd.create_adrv_messages(self.packer, self.CAN, self.frame,
car_fingerprint=self.CP.carFingerprint, car_fingerprint=self.CP.carFingerprint,
drive_gear=drive_gear) drive_gear=drive_gear,
v_ego=host_speed)
can_sends.extend(adrv_messages) can_sends.extend(adrv_messages)
# The front radar treats ADAS_DRV's 0x100 broadcast as its host heartbeat # The front radar treats ADAS_DRV's 0x100 broadcast as its host heartbeat
# and stops publishing object tracks when it disappears. # and stops publishing object tracks when it disappears.
@@ -1335,6 +1335,7 @@ FW_VERSIONS = {
(Ecu.fwdCamera, 0x7c4, None): [ (Ecu.fwdCamera, 0x7c4, None): [
b'\xf1\x00CL4 MFC AT CAN LHD 1.00 1.02 99210-GG000 240708', b'\xf1\x00CL4 MFC AT CAN LHD 1.00 1.02 99210-GG000 240708',
b'\xf1\x00CL4 MFC AT USA LHD 1.00 1.02 99210-GG000 240708', b'\xf1\x00CL4 MFC AT USA LHD 1.00 1.02 99210-GG000 240708',
b'\xf1\x00CL4 MFC AT USA LHD 1.00 1.04 99210-GG100 251205',
], ],
(Ecu.fwdRadar, 0x7d0, None): [ (Ecu.fwdRadar, 0x7d0, None): [
b'\xf1\x00CL4_ RDR ----- 1.00 1.01 99110-GG000 ', b'\xf1\x00CL4_ RDR ----- 1.00 1.01 99110-GG000 ',
@@ -21,7 +21,8 @@ def cache_adrv_0x51_template(car_fingerprint: CAR, dat: bytes | None) -> None:
_adrv_0x51_templates[car_fingerprint] = bytes(dat) _adrv_0x51_templates[car_fingerprint] = bytes(dat)
def create_adrv_0x51(packer, CAN, frame: int, car_fingerprint: CAR | None = None, drive_gear: bool = False): def create_adrv_0x51(packer, CAN, frame: int, car_fingerprint: CAR | None = None, drive_gear: bool = False,
v_ego: float | None = None):
template = _adrv_0x51_templates.get(car_fingerprint) template = _adrv_0x51_templates.get(car_fingerprint)
if template is None: if template is None:
return packer.make_can_msg("ADRV_0x51", CAN.ACAN, {}) return packer.make_can_msg("ADRV_0x51", CAN.ACAN, {})
@@ -29,6 +30,9 @@ def create_adrv_0x51(packer, CAN, frame: int, car_fingerprint: CAR | None = None
dat = bytearray(template) dat = bytearray(template)
dat[2] = (template[2] + frame + 1) & 0xFF dat[2] = (template[2] + frame + 1) & 0xFF
dat[3] = (dat[3] & ~0x1) | int(drive_gear) dat[3] = (dat[3] & ~0x1) | int(drive_gear)
if car_fingerprint == CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN and v_ego is not None and np.isfinite(v_ego):
speed_raw = int(np.clip(round(v_ego * 100.0), 0, 65534))
dat[8:10] = speed_raw.to_bytes(2, "little")
crc = hkg_can_fd_checksum(0x51, None, dat) crc = hkg_can_fd_checksum(0x51, None, dat)
dat[0] = crc & 0xFF dat[0] = crc & 0xFF
dat[1] = (crc >> 8) & 0xFF dat[1] = (crc >> 8) & 0xFF
@@ -815,13 +819,14 @@ def create_fca_warning_light(packer, CAN, frame):
return ret return ret
def create_adrv_messages(packer, CAN, frame, blended_hda2=False, car_fingerprint=None, drive_gear=False): def create_adrv_messages(packer, CAN, frame, blended_hda2=False, car_fingerprint=None, drive_gear=False,
v_ego=None):
# messages needed to car happy after disabling # messages needed to car happy after disabling
# the ADAS Driving ECU to do longitudinal control # the ADAS Driving ECU to do longitudinal control
ret = [] ret = []
ret.append(create_adrv_0x51(packer, CAN, frame, car_fingerprint, drive_gear)) ret.append(create_adrv_0x51(packer, CAN, frame, car_fingerprint, drive_gear, v_ego))
if blended_hda2: if blended_hda2:
return ret return ret
@@ -1796,6 +1796,24 @@ class TestHyundaiFingerprint:
assert exact assert exact
assert matches == {candidate} assert matches == {candidate}
@pytest.mark.parametrize("camera_fw", [
b'\xf1\x00CL4 MFC AT CAN LHD 1.00 1.02 99210-GG000 240708',
b'\xf1\x00CL4 MFC AT USA LHD 1.00 1.02 99210-GG000 240708',
b'\xf1\x00CL4 MFC AT USA LHD 1.00 1.04 99210-GG100 251205',
])
@pytest.mark.parametrize("radar_fw", [
b'\xf1\x00CL4_ RDR ----- 1.00 1.01 99110-GG000 ',
b'\xf1\x00CL4_ RDR ----- 1.00 1.01 99110-GG100 ',
])
def test_k4_2025_2026_fw_exact_matches(self, camera_fw, radar_fw):
car_fw = [
CarParams.CarFw(ecu=Ecu.fwdCamera, fwVersion=camera_fw, address=0x7c4, brand="hyundai"),
CarParams.CarFw(ecu=Ecu.fwdRadar, fwVersion=radar_fw, address=0x7d0, brand="hyundai"),
]
exact, matches = match_fw_to_car(car_fw, "", allow_exact=True, allow_fuzzy=False, log=False)
assert exact
assert matches == {CAR.KIA_K4_2025}
def test_staria_2023_australian_route_fw_exact_matches(self): def test_staria_2023_australian_route_fw_exact_matches(self):
route_fw = { route_fw = {
(Ecu.fwdCamera, 0x7c4): b'\xf1\x00US4 MFC AT AUS RHD 1.00 1.04 99211-CG000 210819', (Ecu.fwdCamera, 0x7c4): b'\xf1\x00US4 MFC AT AUS RHD 1.00 1.04 99211-CG000 210819',
+1 -1
View File
@@ -605,7 +605,7 @@ class CAR(Platforms):
) )
KIA_K4_2025 = HyundaiCanFDPlatformConfig( KIA_K4_2025 = HyundaiCanFDPlatformConfig(
[ [
HyundaiCarDocs("Kia K4 (without HDA II) 2025", car_parts=CarParts.common([CarHarness.hyundai_a])), HyundaiCarDocs("Kia K4 (without HDA II) 2025-26", car_parts=CarParts.common([CarHarness.hyundai_a])),
HyundaiCarDocs("Kia K4 (with HDA II) 2025", car_parts=CarParts.common([CarHarness.hyundai_r])), HyundaiCarDocs("Kia K4 (with HDA II) 2025", car_parts=CarParts.common([CarHarness.hyundai_r])),
], ],
CarSpecs(mass=2987 * CV.LB_TO_KG, wheelbase=2.72, steerRatio=13.4), CarSpecs(mass=2987 * CV.LB_TO_KG, wheelbase=2.72, steerRatio=13.4),
@@ -48,6 +48,7 @@ class CarController(CarControllerBase):
self.angle_handoff_active = False self.angle_handoff_active = False
self.ascent_angle_initialized = False self.ascent_angle_initialized = False
self.ascent_aol_arm_frames = 0 self.ascent_aol_arm_frames = 0
self.ascent_es_distance_counter_last = None
self.cruise_button_prev = 0 self.cruise_button_prev = 0
self.steer_rate_counter = 0 self.steer_rate_counter = 0
@@ -411,7 +412,12 @@ class CarController(CarControllerBase):
can_sends.append(subarucan.create_es_distance(self.packer, self.frame // 5, CS.es_distance_msg, 0, pcm_cancel_cmd, can_sends.append(subarucan.create_es_distance(self.packer, self.frame // 5, CS.es_distance_msg, 0, pcm_cancel_cmd,
self.CP.openpilotLongitudinalControl, cruise_brake > 0, cruise_throttle)) self.CP.openpilotLongitudinalControl, cruise_brake > 0, cruise_throttle))
else: else:
if pcm_cancel_cmd: cancel_frame_ready = True
if self.CP.carFingerprint == CAR.SUBARU_ASCENT_2023:
stock_counter = CS.es_distance_msg["COUNTER"]
cancel_frame_ready = stock_counter != self.ascent_es_distance_counter_last
self.ascent_es_distance_counter_last = stock_counter
if pcm_cancel_cmd and cancel_frame_ready:
if not (self.CP.flags & SubaruFlags.HYBRID): if not (self.CP.flags & SubaruFlags.HYBRID):
bus = CanBus.alt_for_cp(self.CP) if self.CP.flags & SubaruFlags.GLOBAL_GEN2 else self.main_bus bus = CanBus.alt_for_cp(self.CP) if self.CP.flags & SubaruFlags.GLOBAL_GEN2 else self.main_bus
can_sends.append(subarucan.create_es_distance(self.packer, CS.es_distance_msg["COUNTER"] + 1, CS.es_distance_msg, bus, pcm_cancel_cmd)) can_sends.append(subarucan.create_es_distance(self.packer, CS.es_distance_msg["COUNTER"] + 1, CS.es_distance_msg, bus, pcm_cancel_cmd))
@@ -879,6 +879,69 @@ def test_ascent_hud_waits_for_angle_request():
assert controller._lkas_status_active(CC) assert controller._lkas_status_active(CC)
@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):
CP = CarInterface.get_non_essential_params(platform)
controller = CarController({}, CP)
CC = structs.CarControl()
CC.cruiseControl.cancel = True
CS = SimpleNamespace(
out=structs.CarState(),
es_distance_msg=defaultdict(int, COUNTER=11),
es_dashstatus_msg=defaultdict(int),
es_lkas_state_msg=defaultdict(int),
es_infotainment_msg=defaultdict(int),
)
toggles = SimpleNamespace(subaru_sng=False)
cancel_messages = []
for frame in range(5):
_, sends = controller.update(CC.as_reader(), CS, frame * 10_000_000, toggles)
cancel_messages.extend(msg for msg in sends if msg[0] == 0x221)
assert len(cancel_messages) == (1 if platform == CAR.SUBARU_ASCENT_2023 else 5)
assert all(msg[2] == (CanBus.alt if CP.flags & SubaruFlags.GLOBAL_GEN2 else CanBus.main) for msg in cancel_messages)
parser = CANParser(DBC[platform][Bus.pt], [("ES_Distance", 0)], cancel_messages[0][2])
parser.update([(1, [cancel_messages[0]])])
assert parser.vl["ES_Distance"]["COUNTER"] == 12
assert parser.vl["ES_Distance"]["Cruise_Cancel"] == 1
assert parser.vl["ES_Distance"]["Cruise_Throttle"] == 1818
def test_ascent_cancel_uses_fresh_stock_frames_and_handles_counter_rollover():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
controller = CarController({}, CP)
CC = structs.CarControl()
CC.latActive = True
CS = SimpleNamespace(
out=structs.CarState(vEgoRaw=24.04, steeringAngleDeg=2.93, gearShifter="drive"),
es_distance_msg=defaultdict(int, COUNTER=14),
es_dashstatus_msg=defaultdict(int),
es_lkas_state_msg=defaultdict(int),
es_infotainment_msg=defaultdict(int),
)
CS.out.cruiseState.available = True
toggles = SimpleNamespace(subaru_sng=False)
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("ES_Distance", 0)], CanBus.alt)
cancel_counters = []
for frame, (stock_counter, cancel) in enumerate([
(14, False), (14, True), (15, True), (15, True), (0, True), (0, True),
(1, False), (1, True), (2, True),
]):
CS.es_distance_msg["COUNTER"] = stock_counter
CC.cruiseControl.cancel = cancel
_, sends = controller.update(CC.as_reader(), CS, frame * 10_000_000, toggles)
cancel_messages = [msg for msg in sends if msg[0] == 0x221]
assert len(cancel_messages) <= 1
if cancel_messages:
parser.update([(frame + 1, cancel_messages)])
cancel_counters.append(parser.vl["ES_Distance"]["COUNTER"])
assert parser.vl["ES_Distance"]["Cruise_Cancel"] == 1
assert parser.vl["ES_Distance"]["Cruise_Throttle"] == 1818
assert cancel_counters == [0, 1, 3]
def test_other_angle_cars_keep_lateral_status_behavior(): def test_other_angle_cars_keep_lateral_status_behavior():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_CROSSTREK_2025) CP = CarInterface.get_non_essential_params(CAR.SUBARU_CROSSTREK_2025)
controller = CarController({}, CP) controller = CarController({}, CP)
@@ -10,7 +10,7 @@ from opendbc.car.secoc import add_mac, build_sync_mac
from opendbc.car.interfaces import CarControllerBase from opendbc.car.interfaces import CarControllerBase
from opendbc.car.toyota import toyotacan from opendbc.car.toyota import toyotacan
from opendbc.car.toyota.values import CAR, MIN_ACC_SPEED, NO_STOP_TIMER_CAR, PEDAL_TRANSITION, TSS2_CAR, \ from opendbc.car.toyota.values import CAR, MIN_ACC_SPEED, NO_STOP_TIMER_CAR, PEDAL_TRANSITION, TSS2_CAR, \
CarControllerParams, ToyotaFlags, \ CarControllerParams, ToyotaFlags, ToyotaSafetyFlags, \
UNSUPPORTED_DSU_CAR, LEGACY_PRIUS_CAR, TOYOTA_AUTO_HOLD_CARS, TOYOTA_AUTO_HOLD_AEB_CARS UNSUPPORTED_DSU_CAR, LEGACY_PRIUS_CAR, TOYOTA_AUTO_HOLD_CARS, TOYOTA_AUTO_HOLD_AEB_CARS
from opendbc.can import CANPacker from opendbc.can import CANPacker
@@ -68,6 +68,11 @@ def is_ths_hybrid(CP) -> bool:
return CP.carFingerprint in LEGACY_PRIUS_CAR or is_camry_hybrid(CP) return CP.carFingerprint in LEGACY_PRIUS_CAR or is_camry_hybrid(CP)
def uses_rav4_hybrid_sdsu_longitudinal(CP) -> bool:
return bool(CP.carFingerprint == CAR.TOYOTA_RAV4H and CP.openpilotLongitudinalControl and
CP.safetyConfigs[0].safetyParam & ToyotaSafetyFlags.LONG_FILTER.value)
def should_bypass_toyota_long_pid(CP, starpilot_toggles=None) -> bool: def should_bypass_toyota_long_pid(CP, starpilot_toggles=None) -> bool:
highlander_sdsu = ( highlander_sdsu = (
CP.carFingerprint == CAR.TOYOTA_HIGHLANDER and CP.carFingerprint == CAR.TOYOTA_HIGHLANDER and
@@ -107,7 +112,7 @@ def get_long_tune(CP, params):
kiV = [0.5, 0.25] kiV = [0.5, 0.25]
k_f = 1.0 k_f = 1.0
if is_ths_hybrid(CP): if is_ths_hybrid(CP) or uses_rav4_hybrid_sdsu_longitudinal(CP):
k_f = 0.8 if CP.carFingerprint in LEGACY_PRIUS_CAR else 1.0 k_f = 0.8 if CP.carFingerprint in LEGACY_PRIUS_CAR else 1.0
elif CP.carFingerprint not in TSS2_CAR: elif CP.carFingerprint not in TSS2_CAR:
kiBP = [0., 5., 35.] kiBP = [0., 5., 35.]
+4 -3
View File
@@ -1,6 +1,6 @@
from opendbc.car import Bus, structs, get_safety_config, uds from opendbc.car import Bus, structs, get_safety_config, uds
from opendbc.car.toyota.carstate import CarState from opendbc.car.toyota.carstate import CarState
from opendbc.car.toyota.carcontroller import CarController from opendbc.car.toyota.carcontroller import CarController, uses_rav4_hybrid_sdsu_longitudinal
from opendbc.car.toyota.radar_interface import RadarInterface 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, \ 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, \ MIN_ACC_SPEED, EPS_SCALE, NO_STOP_TIMER_CAR, ANGLE_CONTROL_CAR, \
@@ -176,7 +176,8 @@ class CarInterface(CarInterfaceBase):
# min speed to enable ACC. if car can do stop and go, then set enabling speed # min speed to enable ACC. if car can do stop and go, then set enabling speed
# to a negative value, so it won't matter. # to a negative value, so it won't matter.
ret.minEnableSpeed = -1. if (stop_and_go or ret.enableGasInterceptorDEPRECATED) else MIN_ACC_SPEED rav4_hybrid_sdsu_long_defaults = uses_rav4_hybrid_sdsu_longitudinal(ret)
ret.minEnableSpeed = -1. if (stop_and_go or ret.enableGasInterceptorDEPRECATED or rav4_hybrid_sdsu_long_defaults) else MIN_ACC_SPEED
prius_long_defaults = candidate in LEGACY_PRIUS_CAR and ret.openpilotLongitudinalControl prius_long_defaults = candidate in LEGACY_PRIUS_CAR and ret.openpilotLongitudinalControl
camry_hybrid_long_defaults = (candidate == CAR.TOYOTA_CAMRY and ret.openpilotLongitudinalControl and camry_hybrid_long_defaults = (candidate == CAR.TOYOTA_CAMRY and ret.openpilotLongitudinalControl and
@@ -193,7 +194,7 @@ class CarInterface(CarInterfaceBase):
if ret.flags & ToyotaFlags.HYBRID.value: if ret.flags & ToyotaFlags.HYBRID.value:
ret.longitudinalActuatorDelay = 0.05 ret.longitudinalActuatorDelay = 0.05
if camry_hybrid_long_defaults: if camry_hybrid_long_defaults or rav4_hybrid_sdsu_long_defaults:
# The THS eCVT responds much faster than the legacy non-TSS2 ICE tune. # The THS eCVT responds much faster than the legacy non-TSS2 ICE tune.
ret.longitudinalActuatorDelay = 0.05 ret.longitudinalActuatorDelay = 0.05
ret.vEgoStopping = 0.25 ret.vEgoStopping = 0.25
@@ -248,12 +248,12 @@ class CarController(CarControllerBase):
# LCA_5 (formerly SPEED_1) - 0x67 - 50 Hz # LCA_5 (formerly SPEED_1) - 0x67 - 50 Hz
# Contains wheel speeds + LCA signals (LCA_TURN_BITS, LCA_5_STEER) # Contains wheel speeds + LCA signals (LCA_TURN_BITS, LCA_5_STEER)
if self.frame % 2 == 0: # 50 Hz if self.frame % 2 == 0: # 50 Hz
# Initialize counter from CarState on first run if not lat_active or self.lca_5_counter is None:
if self.lca_5_counter is None:
self.lca_5_counter = CS.msg_lca_5['COUNTER'] self.lca_5_counter = CS.msg_lca_5['COUNTER']
# Increment counter by +4, wrap at 15 (0xF never used) # Increment counter by +4, wrap at 15 (0xF never used)
self.lca_5_counter = (self.lca_5_counter + 4) % 15 if lat_active:
self.lca_5_counter = (self.lca_5_counter + 4) % 15
can_sends.append(create_lca_5_message(self.packer, lat_active, apply_angle, can_sends.append(create_lca_5_message(self.packer, lat_active, apply_angle,
CS.msg_lca_5, self.lca_5_counter)) CS.msg_lca_5, self.lca_5_counter))
@@ -3,6 +3,7 @@ from types import SimpleNamespace
import pytest import pytest
from opendbc.can.parser import CANParser
from opendbc.car.volvo.carcontroller import CarController from opendbc.car.volvo.carcontroller import CarController
from opendbc.car.volvo.helpers import checksum_lca_5_message from opendbc.car.volvo.helpers import checksum_lca_5_message
from opendbc.car.volvo.interface import CarInterface from opendbc.car.volvo.interface import CarInterface
@@ -56,21 +57,36 @@ def test_controller_emits_valid_eight_byte_messages_and_lca5_checksum():
assert data[2] == checksum_lca_5_message(data[0], data[1], data[3], data[4], data[5]) assert data[2] == checksum_lca_5_message(data[0], data[1], data[3], data[4], data[5])
def test_controller_relays_stock_lca5_angle_when_inactive(): @pytest.mark.parametrize("fingerprint", [CAR.POLESTAR_2, CAR.VOLVO_XC40_RECHARGE])
cp = CarInterface.get_non_essential_params("VOLVO_XC40_RECHARGE") def test_controller_relays_complete_stock_lca5_when_inactive(fingerprint):
cp = CarInterface.get_non_essential_params(fingerprint)
controller = CarController(DBC[cp.carFingerprint], cp) controller = CarController(DBC[cp.carFingerprint], cp)
parser = CANParser("volvo_mid_1", [("LCA_5", 50)], 0)
stock_data = bytes.fromhex("88c04cef1190ba00")
parser.update([0, [(0x67, stock_data, 0)]])
cs = _state() cs = _state()
cs.msg_lca_5["LCA_5_STEER"] = 12.0 cs.msg_lca_5 = parser.vl["LCA_5"]
cc = SimpleNamespace(latActive=False, actuators=_Actuators()) cc = SimpleNamespace(latActive=False, actuators=_Actuators())
safety = libsafety_py.libsafety
config = cp.safetyConfigs[0]
assert safety.set_safety_hooks(config.safetyModel.raw, config.safetyParam) == 0
safety.init_tests()
safety.set_controls_allowed(False)
safety.safety_rx_hook(libsafety_py.make_CANPacket(0x67, 0, stock_data))
_, can_sends = controller.update(cc, cs, 0, None) _, can_sends = controller.update(cc, cs, 0, None)
lca5 = next(msg for msg in can_sends if msg[0] == 0x67) lca5 = next(msg for msg in can_sends if msg[0] == 0x67)
assert lca5[1] == stock_data
assert safety.safety_tx_hook(libsafety_py.make_CANPacket(lca5[0], lca5[2], lca5[1]))
# The inactive path must not manufacture a new angle command. cs.msg_lca_5["COUNTER"] = 7
raw = ((lca5[1][6] & 0x7F) << 8) | lca5[1][7] controller.update(cc, cs, 0, None)
if raw & (1 << 14): controller.update(cc, cs, 0, None)
raw -= 1 << 15 controller.update(cc, cs, 0, None)
assert abs(raw * 0.05596 - 12.0) < 0.1 cc.latActive = True
_, can_sends = controller.update(cc, cs, 0, None)
active_lca5 = next(msg for msg in can_sends if msg[0] == 0x67)
assert active_lca5[1][3] >> 4 == 11
@pytest.mark.parametrize("fingerprint", [CAR.POLESTAR_2, CAR.VOLVO_XC40_RECHARGE]) @pytest.mark.parametrize("fingerprint", [CAR.POLESTAR_2, CAR.VOLVO_XC40_RECHARGE])
+4 -1
View File
@@ -250,6 +250,9 @@ def create_lca_5_message(packer, lat_active: bool, target_angle_deg: float, msg_
CAN message for LCA_5 on bus 2 CAN message for LCA_5 on bus 2
""" """
if not lat_active:
return packer.make_can_msg('LCA_5', 2, msg_lca_5)
# DBC defines LCA_5_STEER as 15-bit signed with scale 0.05596 deg/count # DBC defines LCA_5_STEER as 15-bit signed with scale 0.05596 deg/count
# Packer handles the encoding automatically - just pass the angle in degrees # Packer handles the encoding automatically - just pass the angle in degrees
@@ -261,7 +264,7 @@ def create_lca_5_message(packer, lat_active: bool, target_angle_deg: float, msg_
'WHEEL_SPEED_2': msg_lca_5['WHEEL_SPEED_2'], 'WHEEL_SPEED_2': msg_lca_5['WHEEL_SPEED_2'],
'NEW_SIGNAL_5': msg_lca_5['NEW_SIGNAL_5'], 'NEW_SIGNAL_5': msg_lca_5['NEW_SIGNAL_5'],
'NEW_SIGNAL_2': msg_lca_5['NEW_SIGNAL_2'], 'NEW_SIGNAL_2': msg_lca_5['NEW_SIGNAL_2'],
'LCA_5_STEER': target_angle_deg if lat_active else msg_lca_5['LCA_5_STEER'], 'LCA_5_STEER': target_angle_deg,
'COUNTER': counter, 'COUNTER': counter,
} }
+41 -3
View File
@@ -71,6 +71,27 @@ static uint16_t volvo_ecm_1_addr;
static uint16_t volvo_bus1_cruise_control_addr; static uint16_t volvo_bus1_cruise_control_addr;
static bool volvo_c1; static bool volvo_c1;
#define VOLVO_STOCK_LCA5_FRAMES 4U
#define VOLVO_STOCK_LCA5_MAX_AGE_US 100000U
static uint8_t volvo_stock_lca5_data[VOLVO_STOCK_LCA5_FRAMES][8];
static uint32_t volvo_stock_lca5_ts[VOLVO_STOCK_LCA5_FRAMES];
static bool volvo_stock_lca5_valid[VOLVO_STOCK_LCA5_FRAMES];
static uint8_t volvo_stock_lca5_index;
static bool volvo_lca5_stock_relay(const CANPacket_t *msg) {
const uint32_t now = microsecond_timer_get();
bool matches_stock = false;
for (uint8_t i = 0U; i < VOLVO_STOCK_LCA5_FRAMES; i++) {
bool matches = volvo_stock_lca5_valid[i] &&
(safety_get_ts_elapsed(now, volvo_stock_lca5_ts[i]) <= VOLVO_STOCK_LCA5_MAX_AGE_US);
for (uint8_t byte = 0U; byte < 8U; byte++) {
matches &= msg->data[byte] == volvo_stock_lca5_data[i][byte];
}
matches_stock |= matches;
}
return matches_stock;
}
static int volvo_be_15(const CANPacket_t *msg, uint8_t byte) { static int volvo_be_15(const CANPacket_t *msg, uint8_t byte) {
return (int)(((uint16_t)(msg->data[byte] & 0x7FU) << 8U) | msg->data[byte + 1U]); return (int)(((uint16_t)(msg->data[byte] & 0x7FU) << 8U) | msg->data[byte + 1U]);
} }
@@ -159,6 +180,15 @@ static void volvo_rx_hook(const CANPacket_t *msg) {
// Main bus (bus 0) messages // Main bus (bus 0) messages
if (msg->bus == VOLVO_MAIN_BUS) { if (msg->bus == VOLVO_MAIN_BUS) {
if (msg->addr == VOLVO_LCA_5) {
for (uint8_t byte = 0U; byte < 8U; byte++) {
volvo_stock_lca5_data[volvo_stock_lca5_index][byte] = msg->data[byte];
}
volvo_stock_lca5_ts[volvo_stock_lca5_index] = microsecond_timer_get();
volvo_stock_lca5_valid[volvo_stock_lca5_index] = true;
volvo_stock_lca5_index = (volvo_stock_lca5_index + 1U) % VOLVO_STOCK_LCA5_FRAMES;
}
// Update brake pedal and cruise state from BCM2 // Update brake pedal and cruise state from BCM2
if (msg->addr == VOLVO_LCA_2) { if (msg->addr == VOLVO_LCA_2) {
// DBC: SG_ BRAKE_PEDAL_PRESSED_A : 47|1@0+ (-1,1) - inverted in DBC, so we invert raw bit // DBC: SG_ BRAKE_PEDAL_PRESSED_A : 47|1@0+ (-1,1) - inverted in DBC, so we invert raw bit
@@ -263,9 +293,13 @@ static bool volvo_tx_hook(const CANPacket_t *msg) {
// LCA frame also contains an angle-shaped field, but the imported controller // LCA frame also contains an angle-shaped field, but the imported controller
// deliberately leaves that field at the observed vehicle value. // deliberately leaves that field at the observed vehicle value.
if (msg->addr == VOLVO_LCA_5) { if (msg->addr == VOLVO_LCA_5) {
const int desired_angle = volvo_lca_5_angle(msg); if (!controls_allowed && volvo_lca5_stock_relay(msg)) {
tx &= SAFETY_ABS(desired_angle) <= VOLVO_MAX_ANGLE_CAN; desired_angle_last = SAFETY_CLAMP(angle_meas.values[0], -VOLVO_MAX_ANGLE_CAN, VOLVO_MAX_ANGLE_CAN);
tx &= !steer_angle_cmd_checks(desired_angle, controls_allowed, VOLVO_ANGLE_STEERING_LIMITS); } else {
const int desired_angle = volvo_lca_5_angle(msg);
tx &= SAFETY_ABS(desired_angle) <= VOLVO_MAX_ANGLE_CAN;
tx &= !steer_angle_cmd_checks(desired_angle, controls_allowed, VOLVO_ANGLE_STEERING_LIMITS);
}
} }
// Keep the two torque-authority arms and the companion LCA angle bounded even // Keep the two torque-authority arms and the companion LCA angle bounded even
@@ -357,6 +391,10 @@ static bool volvo_tx_hook(const CANPacket_t *msg) {
static safety_config volvo_init(uint16_t param) { static safety_config volvo_init(uint16_t param) {
bool spa = GET_FLAG(param, VOLVO_FLAG_SPA); bool spa = GET_FLAG(param, VOLVO_FLAG_SPA);
volvo_c1 = GET_FLAG(param, VOLVO_FLAG_C1); volvo_c1 = GET_FLAG(param, VOLVO_FLAG_C1);
volvo_stock_lca5_index = 0U;
for (uint8_t i = 0U; i < VOLVO_STOCK_LCA5_FRAMES; i++) {
volvo_stock_lca5_valid[i] = false;
}
if (volvo_c1) { if (volvo_c1) {
static const CanMsg VOLVO_C1_TX_MSGS[] = { static const CanMsg VOLVO_C1_TX_MSGS[] = {
@@ -177,6 +177,68 @@ class TestVolvoSafetyBase(common.CarSafetyTest):
self.assertTrue(self._tx(self._angle_cmd_msg(10))) self.assertTrue(self._tx(self._angle_cmd_msg(10)))
self.assertFalse(self._tx(self._angle_cmd_msg(20))) self.assertFalse(self._tx(self._angle_cmd_msg(20)))
STOCK_LCA5 = bytes.fromhex("88c04cef1190ba00")
def _stock_lca5(self, data=None, bus=VOLVO_PARTY_BUS):
return libsafety_py.make_CANPacket(VOLVO_LCA_5, bus, self.STOCK_LCA5 if data is None else data)
def test_inactive_lca5_stock_relay_requires_unchanged_received_frame(self):
self._reset_angle_measurement(10)
self.safety.set_timer(100000)
self.assertFalse(self._tx(self._stock_lca5()))
self._rx(self._stock_lca5(bus=VOLVO_MAIN_BUS))
self.assertTrue(self._tx(self._stock_lca5()))
self.assertEqual(self.safety.get_desired_angle_last(), round(10 / 0.05596))
for byte in range(8):
altered = bytearray(self.STOCK_LCA5)
altered[byte] ^= 1
self.assertFalse(self._tx(self._stock_lca5(bytes(altered))), f"altered {byte=}")
self.assertFalse(self._tx(self._stock_lca5(bus=VOLVO_MAIN_BUS)))
self.assertFalse(self._tx(self._stock_lca5(bus=VOLVO_PT_BUS)))
def test_inactive_lca5_stock_relay_wrong_rx_bus_and_length(self):
self._rx(self._stock_lca5(bus=VOLVO_PT_BUS))
self._rx(self._stock_lca5(self.STOCK_LCA5[:7], bus=VOLVO_MAIN_BUS))
self.assertFalse(self._tx(self._stock_lca5()))
def test_inactive_lca5_stock_relay_expires_across_timer_wrap(self):
start = 0xFFFF0000
self.safety.set_timer(start)
self._rx(self._stock_lca5(bus=VOLVO_MAIN_BUS))
self.safety.set_timer((start + 100000) & 0xFFFFFFFF)
self.assertTrue(self._tx(self._stock_lca5()))
self.safety.set_timer((start + 100001) & 0xFFFFFFFF)
self.assertFalse(self._tx(self._stock_lca5()))
def test_inactive_lca5_stock_relay_history_is_bounded(self):
frames = []
for i in range(5):
data = bytearray(self.STOCK_LCA5)
data[3] = (data[3] + i) & 0xFF
frames.append(bytes(data))
self.safety.set_timer(i * 20000)
self._rx(self._stock_lca5(frames[-1], bus=VOLVO_MAIN_BUS))
self.assertFalse(self._tx(self._stock_lca5(frames[0])))
for data in frames[1:]:
self.assertTrue(self._tx(self._stock_lca5(data)))
def test_inactive_lca5_stock_relay_cleared_on_safety_init(self):
self._rx(self._stock_lca5(bus=VOLVO_MAIN_BUS))
self.assertTrue(self._tx(self._stock_lca5()))
self.safety.set_safety_hooks(SAFETY_VOLVO, self.SAFETY_PARAM)
self.assertFalse(self._tx(self._stock_lca5()))
def test_stock_lca5_placeholder_does_not_bypass_active_angle_limits(self):
self._reset_angle_measurement(10)
self._reset_speed_measurement(50)
self._rx(self._stock_lca5(bus=VOLVO_MAIN_BUS))
self.assertTrue(self._tx(self._stock_lca5()))
self.safety.set_controls_allowed(True)
self.assertFalse(self._tx(self._stock_lca5()))
self.assertTrue(self._tx(self._angle_cmd_msg(10.4)))
self.assertFalse(self._tx(self._angle_cmd_msg(11)))
def test_angle_tx_rate_matches_controller_cadence(self): def test_angle_tx_rate_matches_controller_cadence(self):
"""LCA_5 is 50 Hz, so each frame may contain two 100 Hz controller steps.""" """LCA_5 is 50 Hz, so each frame may contain two 100 Hz controller steps."""
self._reset_speed_measurement(50) self._reset_speed_measurement(50)
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,2 +1,2 @@
extern const uint8_t gitversion[19]; extern const uint8_t gitversion[19];
const uint8_t gitversion[19] = "DEV-96ef704d-DEBUG"; const uint8_t gitversion[19] = "DEV-d2cd3fd0-DEBUG";
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
DEV-96ef704d-DEBUG DEV-d2cd3fd0-DEBUG
+6 -9
View File
@@ -118,17 +118,14 @@ class TestVCruiseHelper:
) )
assert pressed == (self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last) assert pressed == (self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last)
@pytest.mark.parametrize( def test_accel_stops_at_slc_target_before_crossing_it(self):
("starting_kph", "waypoint_kph", "expected_speeds"),
[(30, 33, (33, 35, 40)), (65, 68, (68, 70, 75))],
)
def test_accel_stops_at_slc_target_before_crossing_it(self, starting_kph, waypoint_kph, expected_speeds):
self.starpilot_toggles.cruise_increase = 5 self.starpilot_toggles.cruise_increase = 5
self.v_cruise_helper.v_cruise_kph = starting_kph self.v_cruise_helper.v_cruise_kph = 30
self.v_cruise_helper.v_cruise_cluster_kph = starting_kph self.v_cruise_helper.v_cruise_cluster_kph = 30
slc_target_with_offset = waypoint_kph * CV.KPH_TO_MS slc_target_with_offset = 33 * CV.KPH_TO_MS
for expected_kph in expected_speeds: # A 30 km/h limit with a +3 km/h SLC offset should be an intermediate stop.
for expected_kph in (33, 35, 40):
for pressed in (True, False): for pressed in (True, False):
CS = car.CarState(cruiseState={"available": True}) CS = car.CarState(cruiseState={"available": True})
CS.buttonEvents = [ButtonEvent(type=ButtonType.accelCruise, pressed=pressed)] CS.buttonEvents = [ButtonEvent(type=ButtonType.accelCruise, pressed=pressed)]
+11 -12
View File
@@ -31,7 +31,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle
from openpilot.selfdrive.controls.lib.steering_saturation import is_angle_steering_limited from openpilot.selfdrive.controls.lib.steering_saturation import is_angle_steering_limited
from openpilot.selfdrive.controls.lib.latcontrol_curvature import LatControlCurvature from openpilot.selfdrive.controls.lib.latcontrol_curvature import LatControlCurvature
from openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes import GENESIS_GV70_CARS, GenesisGV70HighwayCommandStabilizer from openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes import get_genesis_highway_command_stabilizer
from openpilot.selfdrive.controls.lib.latcontrol_torque import ( from openpilot.selfdrive.controls.lib.latcontrol_torque import (
BOLT_2018_2021_STEER_RATIO_TEST_SCALE, BOLT_2018_2021_STEER_RATIO_TEST_SCALE,
LatControlTorque, LatControlTorque,
@@ -427,9 +427,8 @@ class Controls:
self.LaC = LatControlPID(self.CP, self.CI, DT_CTRL) self.LaC = LatControlPID(self.CP, self.CI, DT_CTRL)
elif self.CP.lateralTuning.which() == 'torque': elif self.CP.lateralTuning.which() == 'torque':
self.LaC = LatControlTorque(self.CP, self.CI, DT_CTRL) self.LaC = LatControlTorque(self.CP, self.CI, DT_CTRL)
self.gv70_highway_stabilizer = (GenesisGV70HighwayCommandStabilizer() self.genesis_highway_stabilizer = get_genesis_highway_command_stabilizer(
if self.CP.carFingerprint in GENESIS_GV70_CARS and self.CP.lateralTuning.which() == 'torque' self.CP.carFingerprint, self.CP.lateralTuning.which() == 'torque')
else None)
self.sm = self.sm.extend(['liveDelay', 'starpilotCarState', 'starpilotPlan']) self.sm = self.sm.extend(['liveDelay', 'starpilotCarState', 'starpilotPlan'])
@@ -751,14 +750,14 @@ class Controls:
bool(CS.leftBlinker or CS.rightBlinker), bool(CS.leftBlinker or CS.rightBlinker),
bool(CS.steeringPressed)) bool(CS.steeringPressed))
if self.gv70_highway_stabilizer is not None: if self.genesis_highway_stabilizer is not None:
stabilize_gv70 = (CC.latActive and isinstance(self.LaC, LatControlTorque) and stabilize_genesis = (CC.latActive and isinstance(self.LaC, LatControlTorque) and
not CS.steeringPressed and not CS.leftBlinker and not CS.rightBlinker and not CS.steeringPressed and not CS.leftBlinker and not CS.rightBlinker and
not self.starpilot_toggles.lane_centering and not self.starpilot_toggles.lane_centering and
model_v2.meta.laneChangeState == LaneChangeState.off and model_v2.meta.laneChangeState == LaneChangeState.off and
self.sm.all_checks(['modelV2'])) self.sm.all_checks(['modelV2']))
new_desired_curvature = self.gv70_highway_stabilizer.update( new_desired_curvature = self.genesis_highway_stabilizer.update(
new_desired_curvature, CS.vEgo, stabilize_gv70, DT_CTRL) new_desired_curvature, CS.vEgo, stabilize_genesis, DT_CTRL)
jerk_factor = 1.0 jerk_factor = 1.0
if self.starpilot_toggles.lane_change_pace < 10: if self.starpilot_toggles.lane_change_pace < 10:
+13 -1
View File
@@ -23,6 +23,9 @@ _CENTER_ERROR_DEADBAND = 0.08
_E2E_MAX_PATH_STD = 0.35 _E2E_MAX_PATH_STD = 0.35
_E2E_BREAK_IN_START = 0.15 _E2E_BREAK_IN_START = 0.15
_E2E_BREAK_IN_FULL = 0.50 _E2E_BREAK_IN_FULL = 0.50
_E2E_MIN_LANE_AUTHORITY = 0.20
_E2E_BOUNDARY_LANE_AUTHORITY = 0.50
_E2E_BOUNDARY_MARGIN = 0.40
class LaneCenteringController: class LaneCenteringController:
@@ -146,7 +149,16 @@ class LaneCenteringController:
0.0, 0.0,
1.0, 1.0,
) )
error *= 1.0 - e2e_authority * float(break_in) path_clearance = min(model_y - left, right - model_y)
boundary_weight = float(np.clip(
(_MIN_CENTER_TO_LINE + _E2E_BOUNDARY_MARGIN - path_clearance) / _E2E_BOUNDARY_MARGIN,
0.0,
1.0,
))
lane_authority = _E2E_MIN_LANE_AUTHORITY + boundary_weight * (
_E2E_BOUNDARY_LANE_AUTHORITY - _E2E_MIN_LANE_AUTHORITY
)
error *= 1.0 - e2e_authority * float(break_in) * (1.0 - lane_authority)
except (AttributeError, TypeError, ValueError): except (AttributeError, TypeError, ValueError):
pass pass
+6
View File
@@ -8,6 +8,7 @@ from openpilot.selfdrive.controls.lib.latcontrol import LatControl
from openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes import ( from openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes import (
RAV4_TSS2_CARS, RAV4_TSS2_CARS,
SUBARU_IMPREZA_CARS, SUBARU_IMPREZA_CARS,
get_honda_crv_5g_pid_kp_scale,
get_honda_crv_5g_pid_output, get_honda_crv_5g_pid_output,
get_rav4_tss2_pid_output, get_rav4_tss2_pid_output,
get_subaru_impreza_pid_output_scale, get_subaru_impreza_pid_output_scale,
@@ -105,6 +106,7 @@ class LatControlPID(LatControl):
self.honda_lateral_pid_ki_scale = 1.0 self.honda_lateral_pid_ki_scale = 1.0
self.is_civic_bosch_modified = CP.carFingerprint == HONDA.HONDA_CIVIC_BOSCH and bool(CP.flags & HondaFlags.EPS_MODIFIED) self.is_civic_bosch_modified = CP.carFingerprint == HONDA.HONDA_CIVIC_BOSCH and bool(CP.flags & HondaFlags.EPS_MODIFIED)
self.is_honda_crv_5g = CP.carFingerprint == HONDA.HONDA_CRV_5G self.is_honda_crv_5g = CP.carFingerprint == HONDA.HONDA_CRV_5G
self.is_honda_crv_5g_stock_eps = self.is_honda_crv_5g and not bool(CP.flags & HondaFlags.EPS_MODIFIED)
self.is_subaru_impreza = CP.carFingerprint in SUBARU_IMPREZA_CARS self.is_subaru_impreza = CP.carFingerprint in SUBARU_IMPREZA_CARS
self.is_rav4_tss2 = CP.carFingerprint in RAV4_TSS2_CARS self.is_rav4_tss2 = CP.carFingerprint in RAV4_TSS2_CARS
self.prev_angle_steers_des_no_offset = 0.0 self.prev_angle_steers_des_no_offset = 0.0
@@ -163,6 +165,10 @@ class LatControlPID(LatControl):
freeze_integrator = steer_limited_by_safety or steering_pressed or CS.vEgo < 5 freeze_integrator = steer_limited_by_safety or steering_pressed or CS.vEgo < 5
if self.is_honda_crv_5g_stock_eps:
kp_scale = self.honda_lateral_pid_kp_scale * get_honda_crv_5g_pid_kp_scale(angle_steers_des_no_offset, CS.vEgo)
self.pid._k_p = [self.base_kp_bp, scale_lateral_pid_gain_values(self.base_kp_v, kp_scale)]
output_torque = self.pid.update(error, output_torque = self.pid.update(error,
feedforward=ff, feedforward=ff,
speed=CS.vEgo, speed=CS.vEgo,
@@ -280,7 +280,9 @@ GENESIS_GV70_HIGHWAY_STABILIZER_CENTER_LAT_BP = [0.75, 1.0]
GENESIS_GV70_HIGHWAY_STABILIZER_BASELINE_RC = 0.85 GENESIS_GV70_HIGHWAY_STABILIZER_BASELINE_RC = 0.85
GENESIS_GV70_HIGHWAY_STABILIZER_BLEND_RC = 0.35 GENESIS_GV70_HIGHWAY_STABILIZER_BLEND_RC = 0.35
GENESIS_GV70_HIGHWAY_STABILIZER_REVERSAL_LAT = 0.06 GENESIS_GV70_HIGHWAY_STABILIZER_REVERSAL_LAT = 0.06
GENESIS_GV70_HIGHWAY_STABILIZER_REVERSAL_WINDOW = 4.0 GENESIS_GV70_HIGHWAY_STABILIZER_REVERSAL_WINDOW = 6.0
GENESIS_GV70_HIGHWAY_STABILIZER_RECOVERY_SECONDS = 4.0
GENESIS_GV70_HIGHWAY_STABILIZER_DIRECTION_CHANGE_LAT = 0.35
GENESIS_GV70_HIGHWAY_STABILIZER_REDUCTION = 0.70 GENESIS_GV70_HIGHWAY_STABILIZER_REDUCTION = 0.70
GENESIS_GV70_HIGHWAY_STABILIZER_MAX_DELTA = 0.20 GENESIS_GV70_HIGHWAY_STABILIZER_MAX_DELTA = 0.20
@@ -362,6 +364,8 @@ GENESIS_G70_OUTPUT_SMOOTHING_CENTER_RC = 0.18
GENESIS_G70_OUTPUT_SMOOTHING_CURVE_RC = 0.10 GENESIS_G70_OUTPUT_SMOOTHING_CURVE_RC = 0.10
GENESIS_G70_OUTPUT_SMOOTHING_RELEASE_RC = 0.03 GENESIS_G70_OUTPUT_SMOOTHING_RELEASE_RC = 0.03
GENESIS_G70_OUTPUT_SMOOTHING_OVERSHOOT = 0.08 GENESIS_G70_OUTPUT_SMOOTHING_OVERSHOOT = 0.08
GENESIS_G70_HIGHWAY_STABILIZER_CENTER_LAT_BP = [1.4, 1.8]
GENESIS_G70_HIGHWAY_STABILIZER_CURVE_EXIT_LAT = 0.15
GENESIS_G70_ANGLE_OUTPUT_TAPER_MIN = 0.45 GENESIS_G70_ANGLE_OUTPUT_TAPER_MIN = 0.45
GENESIS_G70_ANGLE_OUTPUT_TAPER_START = 70.0 GENESIS_G70_ANGLE_OUTPUT_TAPER_START = 70.0
GENESIS_G70_ANGLE_OUTPUT_TAPER_WIDTH = 6.0 GENESIS_G70_ANGLE_OUTPUT_TAPER_WIDTH = 6.0
@@ -1257,6 +1261,9 @@ HONDA_CRV_5G_PID_CENTER_ANGLE = 14.0
HONDA_CRV_5G_PID_CENTER_ANGLE_WIDTH = 3.0 HONDA_CRV_5G_PID_CENTER_ANGLE_WIDTH = 3.0
HONDA_CRV_5G_PID_OUTPUT_SCALE_MIN = 0.62 HONDA_CRV_5G_PID_OUTPUT_SCALE_MIN = 0.62
HONDA_CRV_5G_PID_OUTPUT_ALPHA_MIN = 0.28 HONDA_CRV_5G_PID_OUTPUT_ALPHA_MIN = 0.28
HONDA_CRV_5G_PID_CENTER_KP_SCALE_MIN = 0.50
HONDA_CRV_5G_PID_CENTER_KP_SPEED_BP = [11.0 * CV.MPH_TO_MS, 18.0 * CV.MPH_TO_MS]
HONDA_CRV_5G_PID_CENTER_KP_ANGLE_BP = [6.0, 18.0]
RAV4_TSS2_CENTER_FRICTION_THRESHOLD_GAIN = 0.14 RAV4_TSS2_CENTER_FRICTION_THRESHOLD_GAIN = 0.14
RAV4_TSS2_CENTER_FRICTION_LAT = 0.30 RAV4_TSS2_CENTER_FRICTION_LAT = 0.30
@@ -1901,6 +1908,12 @@ def get_rav4_tss2_pid_output(output_torque: float, prev_output_torque: float,
return float(prev_output_torque + output_alpha * (limited_output - prev_output_torque)) return float(prev_output_torque + output_alpha * (limited_output - prev_output_torque))
def get_honda_crv_5g_pid_kp_scale(desired_angle_deg: float, v_ego: float) -> float:
speed_weight = np.interp(max(v_ego, 0.0), HONDA_CRV_5G_PID_CENTER_KP_SPEED_BP, [1.0, 0.0])
center_weight = np.interp(abs(desired_angle_deg), HONDA_CRV_5G_PID_CENTER_KP_ANGLE_BP, [1.0, 0.0])
return float(1.0 - (1.0 - HONDA_CRV_5G_PID_CENTER_KP_SCALE_MIN) * speed_weight * center_weight)
def get_honda_crv_5g_pid_output(output_torque: float, prev_output_torque: float, def get_honda_crv_5g_pid_output(output_torque: float, prev_output_torque: float,
desired_angle_deg: float, v_ego: float) -> float: desired_angle_deg: float, v_ego: float) -> float:
"""Damp low-speed CR-V 5G center reversals without blunting real turns.""" """Damp low-speed CR-V 5G center reversals without blunting real turns."""
@@ -3305,8 +3318,10 @@ def get_genesis_gv70_stabilized_output(output_torque: float, prev_output_torque:
return float(output_torque + speed_weight * (smoothed_output - output_torque)) return float(output_torque + speed_weight * (smoothed_output - output_torque))
class GenesisGV70HighwayCommandStabilizer: class GenesisHighwayCommandStabilizer:
def __init__(self) -> None: def __init__(self, center_lat_bp: list[float], curve_exit_lat: float = 0.0) -> None:
self.center_lat_bp = tuple(center_lat_bp)
self.curve_exit_lat = curve_exit_lat
self.reset() self.reset()
def reset(self) -> None: def reset(self) -> None:
@@ -3315,6 +3330,8 @@ class GenesisGV70HighwayCommandStabilizer:
self.reversals: deque[float] = deque() self.reversals: deque[float] = deque()
self.elapsed = 0.0 self.elapsed = 0.0
self.blend = 0.0 self.blend = 0.0
self.active_until = 0.0
self.curve_direction = 0
def update(self, curvature: float, v_ego: float, enabled: bool, dt: float) -> float: def update(self, curvature: float, v_ego: float, enabled: bool, dt: float) -> float:
if not enabled or v_ego <= GENESIS_GV70_HIGHWAY_STABILIZER_SPEED_BP[0] or not math.isfinite(curvature): if not enabled or v_ego <= GENESIS_GV70_HIGHWAY_STABILIZER_SPEED_BP[0] or not math.isfinite(curvature):
@@ -3325,13 +3342,20 @@ class GenesisGV70HighwayCommandStabilizer:
lateral_accel = curvature * v_ego ** 2 lateral_accel = curvature * v_ego ** 2
if self.baseline is None: if self.baseline is None:
self.baseline = lateral_accel self.baseline = lateral_accel
if abs(self.baseline) >= GENESIS_GV70_HIGHWAY_STABILIZER_DIRECTION_CHANGE_LAT:
self.curve_direction = 1 if self.baseline > 0.0 else -1
if self.curve_direction * lateral_accel < 0.0:
self.reset()
self.baseline = lateral_accel
return curvature
self.baseline += dt / (GENESIS_GV70_HIGHWAY_STABILIZER_BASELINE_RC + dt) * (lateral_accel - self.baseline) self.baseline += dt / (GENESIS_GV70_HIGHWAY_STABILIZER_BASELINE_RC + dt) * (lateral_accel - self.baseline)
residual = lateral_accel - self.baseline residual = lateral_accel - self.baseline
if abs(lateral_accel) >= GENESIS_GV70_HIGHWAY_STABILIZER_CENTER_LAT_BP[1]: if abs(lateral_accel) >= self.center_lat_bp[1]:
self.last_sign = 0 self.last_sign = 0
self.reversals.clear() self.reversals.clear()
self.blend = 0.0 self.blend = 0.0
self.active_until = 0.0
return curvature return curvature
sign = 0 sign = 0
@@ -3347,15 +3371,38 @@ class GenesisGV70HighwayCommandStabilizer:
self.reversals.popleft() self.reversals.popleft()
speed_weight = float(np.interp(v_ego, GENESIS_GV70_HIGHWAY_STABILIZER_SPEED_BP, [0.0, 1.0])) speed_weight = float(np.interp(v_ego, GENESIS_GV70_HIGHWAY_STABILIZER_SPEED_BP, [0.0, 1.0]))
target_blend = speed_weight if len(self.reversals) >= 3 else 0.0 if len(self.reversals) >= 3:
self.active_until = self.elapsed + GENESIS_GV70_HIGHWAY_STABILIZER_RECOVERY_SECONDS
target_blend = speed_weight if self.elapsed < self.active_until else 0.0
self.blend += dt / (GENESIS_GV70_HIGHWAY_STABILIZER_BLEND_RC + dt) * (target_blend - self.blend) self.blend += dt / (GENESIS_GV70_HIGHWAY_STABILIZER_BLEND_RC + dt) * (target_blend - self.blend)
center_weight = float(np.interp(abs(lateral_accel), GENESIS_GV70_HIGHWAY_STABILIZER_CENTER_LAT_BP, [1.0, 0.0])) center_weight = float(np.interp(abs(lateral_accel), self.center_lat_bp, [1.0, 0.0]))
if self.curve_direction and self.curve_exit_lat > 0.0:
center_weight *= float(np.interp(abs(lateral_accel), [0.0, self.curve_exit_lat], [0.0, 1.0]))
correction = float(np.clip(GENESIS_GV70_HIGHWAY_STABILIZER_REDUCTION * residual, correction = float(np.clip(GENESIS_GV70_HIGHWAY_STABILIZER_REDUCTION * residual,
-GENESIS_GV70_HIGHWAY_STABILIZER_MAX_DELTA, -GENESIS_GV70_HIGHWAY_STABILIZER_MAX_DELTA,
GENESIS_GV70_HIGHWAY_STABILIZER_MAX_DELTA)) GENESIS_GV70_HIGHWAY_STABILIZER_MAX_DELTA))
return float((lateral_accel - self.blend * center_weight * correction) / v_ego ** 2) return float((lateral_accel - self.blend * center_weight * correction) / v_ego ** 2)
class GenesisGV70HighwayCommandStabilizer(GenesisHighwayCommandStabilizer):
def __init__(self) -> None:
super().__init__(GENESIS_GV70_HIGHWAY_STABILIZER_CENTER_LAT_BP)
class GenesisG70HighwayCommandStabilizer(GenesisHighwayCommandStabilizer):
def __init__(self) -> None:
super().__init__(GENESIS_G70_HIGHWAY_STABILIZER_CENTER_LAT_BP, GENESIS_G70_HIGHWAY_STABILIZER_CURVE_EXIT_LAT)
def get_genesis_highway_command_stabilizer(car_fingerprint: str, torque_control: bool) -> GenesisHighwayCommandStabilizer | None:
if torque_control:
if car_fingerprint in GENESIS_G70_CARS:
return GenesisG70HighwayCommandStabilizer()
if car_fingerprint in GENESIS_GV70_CARS:
return GenesisGV70HighwayCommandStabilizer()
return None
def get_genesis_g70_friction_threshold(v_ego: float, desired_lateral_accel: float = 0.0, def get_genesis_g70_friction_threshold(v_ego: float, desired_lateral_accel: float = 0.0,
desired_lateral_jerk: float = 0.0) -> float: desired_lateral_jerk: float = 0.0) -> float:
base_threshold = get_standard_friction_threshold(v_ego) base_threshold = get_standard_friction_threshold(v_ego)
@@ -428,9 +428,11 @@ def gen_long_ocp():
class LongitudinalMpc: class LongitudinalMpc:
def __init__(self, mode='acc', dt=DT_MDL): def __init__(self, mode='acc', dt=DT_MDL, *, hold_stopped_lead_position=False, sync_model_lead_filters=False):
self.mode = mode self.mode = mode
self.dt = dt self.dt = dt
self.hold_stopped_lead_position = hold_stopped_lead_position
self.sync_model_lead_filters = sync_model_lead_filters
self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N) self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
self.source = SOURCES[2] self.source = SOURCES[2]
# Initialize smoothing filters with default time constants # Initialize smoothing filters with default time constants
@@ -601,7 +603,7 @@ class LongitudinalMpc:
self.solver.set(i, 'x', self.x0) self.solver.set(i, 'x', self.x0)
@staticmethod @staticmethod
def extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau, v_ego=0.0): def extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau, v_ego=0.0, *, hold_stopped_lead_position=False):
speed_mph = v_ego * CV.MS_TO_MPH speed_mph = v_ego * CV.MS_TO_MPH
bp = [0, 20, 35] bp = [0, 20, 35]
exp_weight = np.interp(speed_mph, bp, [1.0, 1.0, 0.0]) # Full exp at <20, blend to constant at 35 exp_weight = np.interp(speed_mph, bp, [1.0, 1.0, 0.0]) # Full exp at <20, blend to constant at 35
@@ -617,7 +619,10 @@ class LongitudinalMpc:
# Constant acceleration component # Constant acceleration component
v_lead_traj_const = np.clip(v_lead + a_lead * T_IDXS, 0.0, 1e8) v_lead_traj_const = np.clip(v_lead + a_lead * T_IDXS, 0.0, 1e8)
x_lead_traj_const = x_lead + v_lead * T_IDXS + 0.5 * a_lead * T_IDXS**2 position_time = T_IDXS
if hold_stopped_lead_position and a_lead < 0.0:
position_time = np.minimum(T_IDXS, max(v_lead, 0.0) / -a_lead)
x_lead_traj_const = x_lead + v_lead * position_time + 0.5 * a_lead * position_time**2
# Blend based on weight # Blend based on weight
v_lead_traj = exp_weight * v_lead_traj_exp + (1 - exp_weight) * v_lead_traj_const v_lead_traj = exp_weight * v_lead_traj_exp + (1 - exp_weight) * v_lead_traj_const
@@ -633,6 +638,18 @@ class LongitudinalMpc:
if lead_active: if lead_active:
model_lead_xv = build_model_lead_trajectory(model_lead, lead, v_ego) model_lead_xv = build_model_lead_trajectory(model_lead, lead, v_ego)
if model_lead_xv is not None: if model_lead_xv is not None:
if self.sync_model_lead_filters:
a_lead = soften_far_radar_lead_accel(
lead.dRel, lead.vLead, lead.aLeadK, v_ego,
get_T_FOLLOW() if t_follow is None else t_follow,
radar=bool(getattr(lead, "radar", False)),
)
self.lead_a_filter.update(float(np.clip(a_lead, -10., 5.)))
self.lead_v_filter.update(float(np.clip(lead.vLead, 0.0, 1e8)))
for lead_filter in (self.duplicate_lead_x_filters[lead_index],
self.duplicate_lead_a_filters[lead_index],
self.duplicate_lead_v_filters[lead_index]):
lead_filter.initialized = False
return model_lead_xv return model_lead_xv
if lead_active: if lead_active:
@@ -688,7 +705,8 @@ class LongitudinalMpc:
self.duplicate_lead_x_filters[lead_index].initialized = False self.duplicate_lead_x_filters[lead_index].initialized = False
self.duplicate_lead_a_filters[lead_index].initialized = False self.duplicate_lead_a_filters[lead_index].initialized = False
self.duplicate_lead_v_filters[lead_index].initialized = False self.duplicate_lead_v_filters[lead_index].initialized = False
lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau, v_ego) lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau, v_ego,
hold_stopped_lead_position=self.hold_stopped_lead_position)
return lead_xv return lead_xv
@staticmethod @staticmethod
@@ -36,6 +36,9 @@ from openpilot.selfdrive.controls.lib.longitudinal_vehicle_tunes import (
is_toyota_rav4_tss2_post_departure_tune, is_toyota_rav4_tss2_post_departure_tune,
get_toyota_rav4_tss2_early_lead_cap, get_toyota_rav4_tss2_early_lead_cap,
get_toyota_corolla_braking_lead_cap, get_toyota_corolla_braking_lead_cap,
is_toyota_corolla_early_radar_follow_lead,
use_stopped_lead_position,
use_model_lead_filter_sync,
is_toyota_rav4_tss2_radar_follow_lead, is_toyota_rav4_tss2_radar_follow_lead,
get_toyota_sienna_post_departure_restop_cap, get_toyota_sienna_post_departure_restop_cap,
get_untracked_slow_lead_decel_scale, get_untracked_slow_lead_decel_scale,
@@ -579,7 +582,8 @@ def get_accel_from_plan(speeds, accels, action_t=DT_MDL, vEgoStopping=0.05):
class LongitudinalPlanner: class LongitudinalPlanner:
def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL): def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL):
self.CP = CP self.CP = CP
self.mpc = LongitudinalMpc(dt=dt) self.mpc = LongitudinalMpc(dt=dt, hold_stopped_lead_position=use_stopped_lead_position(CP),
sync_model_lead_filters=use_model_lead_filter_sync(CP))
self.fcw = False self.fcw = False
self.dt = dt self.dt = dt
self.model_allow_throttle = True self.model_allow_throttle = True
@@ -2145,7 +2149,9 @@ class LongitudinalPlanner:
# safety path so ACC/chill does not ignore a visible lead during that debounce. # safety path so ACC/chill does not ignore a visible lead during that debounce.
lead_control_active = ( lead_control_active = (
tracking_lead or raw_close_lead_control or early_truck_follow or rav4_radar_follow or tracking_lead or raw_close_lead_control or early_truck_follow or rav4_radar_follow or
lightning_stopped_radar_follow lightning_stopped_radar_follow or
any(is_toyota_corolla_early_radar_follow_lead(self.CP, lead, scene_v_ego)
for lead in (self.lead_one, self.lead_two))
) )
lead_one_active = bool(self.lead_one.status and lead_control_active) lead_one_active = bool(self.lead_one.status and lead_control_active)
effective_t_follow = self.get_dynamic_t_follow(sm['starpilotPlan'].tFollow, self.lead_one if lead_one_active else None, v_ego) effective_t_follow = self.get_dynamic_t_follow(sm['starpilotPlan'].tFollow, self.lead_one if lead_one_active else None, v_ego)
@@ -139,6 +139,7 @@ TOYOTA_CAMRY_TSS2_FORCE_STOP_DISTANCE_BIAS_M = 6.0
DEFAULT_FORCE_STOP_HANDOFF_M = 6.0 DEFAULT_FORCE_STOP_HANDOFF_M = 6.0
HYUNDAI_SANTA_FE_2022_FORCE_STOP_REANCHOR_SPEED_TOLERANCE = 0.25 HYUNDAI_SANTA_FE_2022_FORCE_STOP_REANCHOR_SPEED_TOLERANCE = 0.25
HYUNDAI_SANTA_FE_2022_FORCE_STOP_LOW_SPEED_HOLD = 2.5 HYUNDAI_SANTA_FE_2022_FORCE_STOP_LOW_SPEED_HOLD = 2.5
FORD_MACH_E_FORCE_STOP_LOW_SPEED_HOLD = 1.0
KIA_CARNIVAL_2025_STOP_SIGN_LOW_SPEED_HOLD = 0.75 KIA_CARNIVAL_2025_STOP_SIGN_LOW_SPEED_HOLD = 0.75
@@ -171,6 +172,34 @@ def get_toyota_prius_stopped_lead_obstacle_bias(CP, lead, v_ego):
return float(min(bias, max(distance - 0.5, 0.0))) return float(min(bias, max(distance - 0.5, 0.0)))
def use_stopped_lead_position(CP):
return (
getattr(CP, "brand", "") == "toyota" and
str(getattr(CP, "carFingerprint", "")) == "TOYOTA_COROLLA_TSS2"
)
def use_model_lead_filter_sync(CP):
return (
getattr(CP, "brand", "") == "toyota" and
str(getattr(CP, "carFingerprint", "")) == "TOYOTA_COROLLA_TSS2"
)
def is_toyota_corolla_early_radar_follow_lead(CP, lead, v_ego):
if (
getattr(CP, "brand", "") != "toyota" or
str(getattr(CP, "carFingerprint", "")) != "TOYOTA_COROLLA_TSS2" or
lead is None or not bool(getattr(lead, "status", False)) or
not bool(getattr(lead, "radar", False)) or
float(getattr(lead, "modelProb", 0.0)) < 0.5 or float(v_ego) < 15.0
):
return False
closing_speed = float(v_ego) - max(float(lead.vLead), 0.0)
return closing_speed >= 7.0 and 30.0 <= float(lead.dRel) <= min(120.0, 4.0 * float(v_ego))
def get_toyota_corolla_braking_lead_cap(CP, lead, v_ego, desired_gap, accel_min): def get_toyota_corolla_braking_lead_cap(CP, lead, v_ego, desired_gap, accel_min):
if ( if (
getattr(CP, "brand", "") != "toyota" or getattr(CP, "brand", "") != "toyota" or
@@ -781,9 +810,11 @@ def get_force_stop_reanchor_speed_tolerance(car_params):
def get_force_stop_low_speed_hold(car_params): def get_force_stop_low_speed_hold(car_params):
"""Keep a committed Santa Fe stop from releasing while it is still rolling.""" fingerprint = str(getattr(car_params, "carFingerprint", car_params))
if str(getattr(car_params, "carFingerprint", car_params)) == "HYUNDAI_SANTA_FE_2022": if fingerprint == "HYUNDAI_SANTA_FE_2022":
return HYUNDAI_SANTA_FE_2022_FORCE_STOP_LOW_SPEED_HOLD return HYUNDAI_SANTA_FE_2022_FORCE_STOP_LOW_SPEED_HOLD
if fingerprint == "FORD_MUSTANG_MACH_E_MK1":
return FORD_MACH_E_FORCE_STOP_LOW_SPEED_HOLD
return None return None
@@ -73,3 +73,83 @@ def test_strong_turn_and_driver_input_reset_stabilizer():
assert update_accel(stabilizer, -0.3, enabled=False) == pytest.approx(-0.3) assert update_accel(stabilizer, -0.3, enabled=False) == pytest.approx(-0.3)
assert update_accel(stabilizer, 0.3) == pytest.approx(0.3) assert update_accel(stabilizer, 0.3) == pytest.approx(0.3)
def test_slow_highway_oscillation_stays_damped_between_reversals():
stabilizer = GenesisGV70HighwayCommandStabilizer()
raw, shaped, blends = [], [], []
for i in range(3000):
accel = 0.50 + 0.20 * math.sin(2.0 * math.pi * 0.28 * i * 0.01)
raw.append(accel)
shaped.append(update_accel(stabilizer, accel))
blends.append(stabilizer.blend)
assert min(blends[1500:]) > 0.99
assert np.std(shaped[1500:]) < 0.70 * np.std(raw[1500:])
assert np.mean(shaped[1500:]) == pytest.approx(np.mean(raw[1500:]), abs=0.015)
def test_stabilizer_recovers_after_oscillation_ends():
stabilizer = GenesisGV70HighwayCommandStabilizer()
for i in range(1600):
update_accel(stabilizer, 0.50 + 0.20 * math.sin(2.0 * math.pi * 0.4 * i * 0.01))
assert stabilizer.blend > 0.9
for _ in range(1500):
shaped = update_accel(stabilizer, 0.50)
assert stabilizer.blend < 0.001
assert shaped == pytest.approx(0.50, abs=1e-6)
@pytest.mark.parametrize('direction', [-1.0, 1.0])
def test_real_curve_direction_change_bypasses_recovery(direction):
stabilizer = GenesisGV70HighwayCommandStabilizer()
for i in range(1600):
update_accel(stabilizer, direction * (0.55 + 0.20 * math.sin(2.0 * math.pi * 0.4 * i * 0.01)))
assert stabilizer.blend > 0.9
assert update_accel(stabilizer, -direction * 0.60) == pytest.approx(-direction * 0.60)
assert stabilizer.blend == 0.0
assert not stabilizer.reversals
assert stabilizer.active_until == 0.0
assert stabilizer.curve_direction == 0
@pytest.mark.parametrize('direction', [-1.0, 1.0])
def test_gradual_s_curve_does_not_delay_direction_change(direction):
stabilizer = GenesisGV70HighwayCommandStabilizer()
for i in range(1600):
update_accel(stabilizer, direction * (0.55 + 0.20 * math.sin(2.0 * math.pi * 0.4 * i * 0.01)))
assert stabilizer.blend > 0.9
raw = direction * np.linspace(0.55, -0.65, 500)
shaped = np.array([update_accel(stabilizer, float(accel)) for accel in raw])
raw_crossing = np.flatnonzero(direction * raw < 0.0)[0]
shaped_crossing = np.flatnonzero(direction * shaped < 0.0)[0]
assert shaped_crossing == raw_crossing
assert shaped[raw_crossing:] == pytest.approx(raw[raw_crossing:])
def test_inactive_reset_clears_recovery_and_reengages_cleanly():
stabilizer = GenesisGV70HighwayCommandStabilizer()
for i in range(1600):
update_accel(stabilizer, 0.50 + 0.20 * math.sin(2.0 * math.pi * 0.4 * i * 0.01))
assert stabilizer.active_until > stabilizer.elapsed
assert update_accel(stabilizer, 0.60, enabled=False) == pytest.approx(0.60)
assert stabilizer.active_until == 0.0
assert stabilizer.baseline is None
assert update_accel(stabilizer, -0.60) == pytest.approx(-0.60)
@pytest.mark.parametrize('speed', [40.1, 45.0, 50.0, 65.0])
def test_recovery_respects_speed_gate_and_correction_bound(speed):
stabilizer = GenesisGV70HighwayCommandStabilizer()
speed *= CV.MPH_TO_MS
max_delta = 0.0
for i in range(1600):
accel = 0.3 * math.sin(2.0 * math.pi * 0.4 * i * 0.01)
shaped = update_accel(stabilizer, accel, speed)
max_delta = max(max_delta, abs(shaped-accel))
speed_weight = np.interp(speed, [40.0*CV.MPH_TO_MS,50.0*CV.MPH_TO_MS], [0.0,1.0])
assert 0.0 < max_delta <= 0.20*speed_weight+1e-6
@@ -0,0 +1,61 @@
import pytest
from openpilot.common.constants import CV
from openpilot.common.pid import PIDController
from openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes import get_honda_crv_5g_pid_kp_scale
@pytest.mark.parametrize('mph', [0.0, 3.0, 6.0, 8.0, 11.0])
@pytest.mark.parametrize('angle', [-6.0, -3.0, 0.0, 3.0, 6.0])
def test_low_speed_center_gain(mph, angle):
assert get_honda_crv_5g_pid_kp_scale(angle, mph * CV.MPH_TO_MS) == 0.5
@pytest.mark.parametrize('mph', [18.0, 25.0, 45.0, 70.0])
@pytest.mark.parametrize('angle', [-30.0, -6.0, 0.0, 6.0, 30.0])
def test_normal_speed_gain_unchanged(mph, angle):
assert get_honda_crv_5g_pid_kp_scale(angle, mph * CV.MPH_TO_MS) == 1.0
@pytest.mark.parametrize('mph', [0.0, 8.0, 14.0])
@pytest.mark.parametrize('angle', [-90.0, -30.0, -18.0, 18.0, 30.0, 90.0])
def test_real_turn_gain_unchanged(mph, angle):
assert get_honda_crv_5g_pid_kp_scale(angle, mph * CV.MPH_TO_MS) == 1.0
def test_gain_is_bounded_symmetric_and_monotonic():
for mph in [0, 8, 11, 12, 14, 16, 18, 45]:
scales = [get_honda_crv_5g_pid_kp_scale(angle, mph * CV.MPH_TO_MS) for angle in [0, 6, 9, 12, 15, 18, 30]]
assert scales == sorted(scales)
assert all(0.5 <= value <= 1.0 for value in scales)
for angle in [0, 6, 9, 12, 15, 18, 30]:
assert get_honda_crv_5g_pid_kp_scale(angle, mph * CV.MPH_TO_MS) == get_honda_crv_5g_pid_kp_scale(-angle, mph * CV.MPH_TO_MS)
scales = [get_honda_crv_5g_pid_kp_scale(0.0, mph * CV.MPH_TO_MS) for mph in [0, 8, 11, 12, 14, 16, 18, 45]]
assert scales == sorted(scales)
@pytest.mark.parametrize('mph', [11.0, 18.0])
def test_speed_boundary_continuity(mph):
left = get_honda_crv_5g_pid_kp_scale(0.0, (mph - 1e-7) * CV.MPH_TO_MS)
right = get_honda_crv_5g_pid_kp_scale(0.0, (mph + 1e-7) * CV.MPH_TO_MS)
assert abs(left - right) < 1e-7
@pytest.mark.parametrize('angle', [-18.0, -6.0, 6.0, 18.0])
def test_angle_boundary_continuity(angle):
left = get_honda_crv_5g_pid_kp_scale(angle - 1e-7, 8.0 * CV.MPH_TO_MS)
right = get_honda_crv_5g_pid_kp_scale(angle + 1e-7, 8.0 * CV.MPH_TO_MS)
assert abs(left - right) < 1e-7
def test_gain_reduces_feedback_before_saturation_without_changing_feedforward_or_integral():
base = PIDController(0.64, 0.192, pos_limit=1, neg_limit=-1)
tuned = PIDController(0.64 * get_honda_crv_5g_pid_kp_scale(3.0, 8.0 * CV.MPH_TO_MS), 0.192, pos_limit=1, neg_limit=-1)
base.i = tuned.i = -0.019
base_output = base.update(2.0, feedforward=0.004, freeze_integrator=True)
tuned_output = tuned.update(2.0, feedforward=0.004, freeze_integrator=True)
assert base_output == 1.0
assert tuned_output == pytest.approx(0.625)
assert tuned.p == pytest.approx(base.p * 0.5)
assert tuned.i == base.i
assert tuned.f == base.f
@@ -139,18 +139,59 @@ def test_offset_is_reduced_in_narrow_lane():
assert np.isclose(at_safe_limit, above_safe_limit) assert np.isclose(at_safe_limit, above_safe_limit)
def test_confident_e2e_path_can_fully_break_in(): @pytest.mark.parametrize("direction", [-1.0, 1.0])
model = _model(left=-1.0, right=2.6, model_y=0.0, path_std=0.1) def test_confident_e2e_path_retains_bounded_lane_correction(direction):
model = _model(left=-2.4, right=2.4, model_y=direction * 0.6, path_std=0.1)
_, lane_authority = _converge(model, authority=0.0) _, lane_authority = _converge(model, authority=0.0)
_, e2e_authority = _converge(model, authority=1.0) _, e2e_authority = _converge(model, authority=1.0)
assert lane_authority > 0.0 assert lane_authority * direction < 0.0
assert abs(e2e_authority) < 1e-9 assert e2e_authority == pytest.approx(0.2 * lane_authority)
@pytest.mark.parametrize("direction", [-1.0, 1.0])
def test_e2e_retains_more_lane_correction_near_boundary(direction):
model = _model(model_y=direction * 0.8, path_std=0.1)
_, lane_authority = _converge(model, authority=0.0)
_, e2e_authority = _converge(model, authority=1.0)
assert e2e_authority == pytest.approx(0.5 * lane_authority)
def test_e2e_boundary_authority_blends_continuously():
fractions = []
for clearance in np.linspace(1.55, 1.05, 101):
model = _model(left=-2.4, right=2.4, model_y=-2.4 + clearance)
lane_valid, lane_authority = LaneCenteringController._raw_correction(model, _V_EGO, 0.0, 0.0)
e2e_valid, e2e_authority = LaneCenteringController._raw_correction(model, _V_EGO, 0.0, 1.0)
assert lane_valid and e2e_valid
fractions.append(e2e_authority / lane_authority)
assert fractions[0] == pytest.approx(0.2)
assert fractions[-1] == pytest.approx(0.5)
assert np.all(np.diff(fractions) >= -1e-9)
assert np.max(np.diff(fractions)) < 0.004
@pytest.mark.parametrize("line", [1, 2])
def test_e2e_boundary_correction_requires_both_lane_lines(line):
model = _model(model_y=-0.8)
assert _update(LaneCenteringController(), model) > 0.0
model.laneLineProbs[line] = 0.59
assert _update(LaneCenteringController(), model) == 0.0
@pytest.mark.parametrize("direction", [-1.0, 1.0])
def test_e2e_boundary_correction_remains_capped_and_yields_to_driver(direction):
model = _model(model_y=direction * 2.0)
controller, output = _converge(model)
assert output * direction < 0.0
assert abs(output) <= 0.004 * 0.30
assert _update(controller, model, driver_override=True) == 0.0
def test_uncertain_e2e_path_does_not_break_in(): def test_uncertain_e2e_path_does_not_break_in():
model = _model(left=-1.0, right=2.6, model_y=0.0, path_std=0.6) model = _model(left=-1.0, right=2.6, model_y=0.0, path_std=0.6)
_, lane_only = _converge(model, authority=0.0)
_, output = _converge(model, authority=1.0) _, output = _converge(model, authority=1.0)
assert output > 0.0 assert output == pytest.approx(lane_only)
def test_e2e_authority_blends_lane_correction(): def test_e2e_authority_blends_lane_correction():
@@ -2092,6 +2092,41 @@ class TestLatControl:
assert lac_log.active assert lac_log.active
assert abs(tuned_output) < abs(base_output) assert abs(tuned_output) < abs(base_output)
def test_honda_crv_5g_pid_center_gain_update_path(self):
controller, VM, CS, params, toggles = self._build_pid_controller(HONDA.HONDA_CRV_5G)
CS.vEgo = 8.0 * 0.44704
CS.steeringAngleDeg = -2.0
controller.pid.i = 0.03
toggles.honda_lateral_pid_kp_scale = 1.2
toggles.honda_lateral_pid_ki_scale = 0.8
for _ in range(20):
_, _, lac_log = controller.update(True, CS, VM, params, False, 0.0, False, 0.2, None, None, toggles)
assert lac_log.p == pytest.approx(controller.base_kp_v[0] * 1.2 * 0.5 * lac_log.angleError)
assert lac_log.i == pytest.approx(0.03)
assert controller.pid._k_i[1] == pytest.approx([value * 0.8 for value in controller.base_ki_v])
CS.vEgo = 25.0 * 0.44704
_, _, lac_log = controller.update(True, CS, VM, params, False, 0.0, False, 0.2, None, None, toggles)
assert lac_log.p == pytest.approx(controller.base_kp_v[0] * 1.2 * lac_log.angleError)
@pytest.mark.parametrize('car_name,eps_modified', [(HONDA.HONDA_CRV_5G, True), (HONDA.HONDA_CIVIC_BOSCH, False),
(TOYOTA.TOYOTA_RAV4_TSS2, False)])
def test_honda_crv_5g_pid_center_gain_does_not_change_other_paths(self, monkeypatch, car_name, eps_modified):
controller, VM, CS, params, toggles = self._build_pid_controller(car_name)
if eps_modified:
CP = interfaces[car_name].get_non_essential_params(car_name)
CP.flags = int(CP.flags | HondaFlags.EPS_MODIFIED)
controller = LatControlPID(CP.as_reader(), interfaces[car_name](CP, custom.StarPilotCarParams.new_message()), DT_CTRL)
CS.vEgo = 8.0 * 0.44704
CS.steeringAngleDeg = -2.0
def unexpected_gain(*_args):
raise AssertionError('CR-V center gain must not run for this controller')
monkeypatch.setattr(latcontrol_pid, 'get_honda_crv_5g_pid_kp_scale', unexpected_gain)
_, _, lac_log = controller.update(True, CS, VM, params, False, 0.0, False, 0.2, None, None, toggles)
assert lac_log.p == pytest.approx(controller.base_kp_v[0] * lac_log.angleError)
def test_rav4_tss2_torque_center_tune_fades_before_real_turns(self): def test_rav4_tss2_torque_center_tune_fades_before_real_turns(self):
low_speed_center = get_rav4_tss2_center_output_scale(0.05, 8.0) low_speed_center = get_rav4_tss2_center_output_scale(0.05, 8.0)
low_speed_turn = get_rav4_tss2_center_output_scale(1.0, 8.0) low_speed_turn = get_rav4_tss2_center_output_scale(1.0, 8.0)
File diff suppressed because it is too large Load Diff
@@ -64,7 +64,6 @@ def make_sm(*, set_speed_kph=100.0, lead_one=None, lead_two=None, standstill=Fal
"carState": SimpleNamespace(vCruise=set_speed_kph, standstill=standstill, vEgoCluster=v_ego_cluster), "carState": SimpleNamespace(vCruise=set_speed_kph, standstill=standstill, vEgoCluster=v_ego_cluster),
"carControl": SimpleNamespace(orientationNED=[0.0, pitch, 0.0]), "carControl": SimpleNamespace(orientationNED=[0.0, pitch, 0.0]),
"controlsState": SimpleNamespace(forceDecel=force_decel), "controlsState": SimpleNamespace(forceDecel=force_decel),
"selfdriveState": SimpleNamespace(personality=1),
"radarState": SimpleNamespace( "radarState": SimpleNamespace(
leadOne=lead_one or make_lead(), leadOne=lead_one or make_lead(),
leadTwo=lead_two or make_lead(), leadTwo=lead_two or make_lead(),
@@ -2,7 +2,6 @@ import datetime
import pytest import pytest
from cereal import custom
from openpilot.common.constants import CV from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL from openpilot.common.realtime import DT_MDL
from openpilot.starpilot.common.starpilot_variables import PLANNER_TIME from openpilot.starpilot.common.starpilot_variables import PLANNER_TIME
@@ -37,26 +36,15 @@ class FakeParams:
def get_float(self, *args, **kwargs): def get_float(self, *args, **kwargs):
return 0.0 return 0.0
def get_bool(self, key):
return bool(self.values.get(key, False))
def remove(self, key):
self.values.pop(key, None)
def put_nonblocking(self, key, value): def put_nonblocking(self, key, value):
self.values[key] = value self.values[key] = value
self.writes.append((key, value)) self.writes.append((key, value))
def put_float(self, key, value):
self.values[key] = value
def make_vcruise(*, red_light=False, raw_model_stopped=False, forcing_stop=False, nav_state=None, road_curvature=0.0): def make_vcruise(*, red_light=False, raw_model_stopped=False, forcing_stop=False, nav_state=None, road_curvature=0.0):
planner = SimpleNamespace( planner = SimpleNamespace(
params=FakeParams(), params=FakeParams(),
params_memory=FakeParams({"NavInstructionState": nav_state or {}}), params_memory=FakeParams({"NavInstructionState": nav_state or {}}),
gps_position={},
gps_valid=False,
lead_one=SimpleNamespace(status=False, dRel=float("inf"), vLead=0.0), lead_one=SimpleNamespace(status=False, dRel=float("inf"), vLead=0.0),
starpilot_cem=SimpleNamespace(stop_light_detected=red_light), starpilot_cem=SimpleNamespace(stop_light_detected=red_light),
starpilot_following=SimpleNamespace(following_lead=False), starpilot_following=SimpleNamespace(following_lead=False),
@@ -89,14 +77,9 @@ def make_sm(*, standstill=True, min_steer_speed=0.0, car_fingerprint=""):
leftBlinker=False, leftBlinker=False,
rightBlinker=False, rightBlinker=False,
steeringAngleDeg=0.0, steeringAngleDeg=0.0,
vCruise=72.0,
), ),
"liveParameters": SimpleNamespace(angleOffsetDeg=0.0),
"mapdOut": SimpleNamespace(nextSpeedLimitDistance=0.0, nextSpeedLimit=0.0, speedLimit=0.0,
waySelectionType=custom.WaySelectionType.fail, roadName=""),
"selfdriveState": SimpleNamespace(enabled=True),
"carParams": SimpleNamespace(minSteerSpeed=min_steer_speed, carFingerprint=car_fingerprint), "carParams": SimpleNamespace(minSteerSpeed=min_steer_speed, carFingerprint=car_fingerprint),
"starpilotCarState": SimpleNamespace(accelPressed=False, decelPressed=False, dashboardStopSign=0, dashboardSpeedLimit=0), "starpilotCarState": SimpleNamespace(accelPressed=False, dashboardStopSign=0, dashboardSpeedLimit=0),
"onroadEvents": [], "onroadEvents": [],
} }
@@ -122,27 +105,6 @@ def make_toggles():
nav_longitudinal_allowed=False, nav_longitudinal_allowed=False,
speed_limit_controller=False, speed_limit_controller=False,
show_speed_limits=False, show_speed_limits=False,
is_metric=False,
map_speed_lookahead_higher=0.0,
map_speed_lookahead_lower=0.0,
slc_fallback_previous_speed_limit=False,
slc_fallback_set_speed=False,
slc_fallback_experimental_mode=False,
slc_mapbox_filler=False,
speed_limit_confirmation_higher=False,
speed_limit_confirmation_lower=False,
speed_limit_priority1="Dashboard",
speed_limit_priority2="Map Data",
speed_limit_priority_highest=False,
speed_limit_priority_lowest=False,
vision_speed_limit_detection=False,
speed_limit_offset1=0.0,
speed_limit_offset2=0.0,
speed_limit_offset3=0.0,
speed_limit_offset4=0.0,
speed_limit_offset5=0.0,
speed_limit_offset6=0.0,
speed_limit_offset7=0.0,
force_stop_distance_offset=0, force_stop_distance_offset=0,
) )
@@ -150,6 +112,7 @@ def make_toggles():
def test_active_slc_control_target_does_not_require_set_speed_limit(): def test_active_slc_control_target_does_not_require_set_speed_limit():
target = get_active_slc_control_target( target = get_active_slc_control_target(
speed_limit_controller=True, speed_limit_controller=True,
set_speed_limit=False,
slc_target=45.0 * CV.MPH_TO_MS, slc_target=45.0 * CV.MPH_TO_MS,
slc_offset=3.0 * CV.MPH_TO_MS, slc_offset=3.0 * CV.MPH_TO_MS,
overridden_speed=0.0, overridden_speed=0.0,
@@ -182,7 +145,8 @@ def test_active_slc_target_constrains_vcruise_below_csc_minimum(slc_target_mph,
vcruise.slc.target = slc_target_mph * CV.MPH_TO_MS vcruise.slc.target = slc_target_mph * CV.MPH_TO_MS
vcruise.slc.source = "Dashboard" vcruise.slc.source = "Dashboard"
vcruise.slc.update = lambda *_args, **_kwargs: None vcruise.slc.update_limits = lambda *_args, **_kwargs: None
vcruise.slc.update_override = lambda *_args, **_kwargs: None
result = update_vcruise( result = update_vcruise(
vcruise, vcruise,
@@ -194,7 +158,6 @@ def test_active_slc_target_constrains_vcruise_below_csc_minimum(slc_target_mph,
) )
assert result == pytest.approx(expected_v_cruise_mph * CV.MPH_TO_MS) assert result == pytest.approx(expected_v_cruise_mph * CV.MPH_TO_MS)
assert vcruise.slc_is_limiting_max_set == (expected_v_cruise_mph < 35.0)
def test_elantra_gets_lead_veto_margin_before_force_stop(): def test_elantra_gets_lead_veto_margin_before_force_stop():
@@ -454,8 +417,6 @@ def test_csc_res_press_defers_to_slc_confirmation():
sm = make_sm(standstill=False) sm = make_sm(standstill=False)
toggles = make_toggles() toggles = make_toggles()
toggles.curve_speed_controller = True toggles.curve_speed_controller = True
toggles.speed_limit_controller = True
toggles.speed_limit_confirmation_higher = True
def set_curve_target(_v_ego, _v_cruise): def set_curve_target(_v_ego, _v_cruise):
vcruise.csc.target = 14.0 vcruise.csc.target = 14.0
@@ -465,12 +426,10 @@ def test_csc_res_press_defers_to_slc_confirmation():
assert vcruise.csc_controlling_speed assert vcruise.csc_controlling_speed
# The candidate and accel press arrive together. SLC consumes the press in vcruise.slc.speed_limit_changed_timer = 1.0
# this frame, even though accepting immediately clears the pending state. vcruise.slc.unconfirmed_speed_limit = 25.0
sm["starpilotCarState"].dashboardSpeedLimit = 45.0 * CV.MPH_TO_MS
sm["starpilotCarState"].accelPressed = True sm["starpilotCarState"].accelPressed = True
update_vcruise(vcruise, sm, toggles, now=80.05, v_ego=20.0) update_vcruise(vcruise, sm, toggles, now=80.05, v_ego=20.0)
assert vcruise.slc.confirmation_button_consumed
assert not vcruise.csc_override assert not vcruise.csc_override
sm["starpilotCarState"].accelPressed = False sm["starpilotCarState"].accelPressed = False
@@ -663,6 +622,7 @@ def test_curve_speed_controller_hysteresis_keeps_glow_off_for_marginal_targets()
def test_active_slc_control_target_applies_offset_and_cluster_diff(): def test_active_slc_control_target_applies_offset_and_cluster_diff():
target = get_active_slc_control_target( target = get_active_slc_control_target(
speed_limit_controller=True, speed_limit_controller=True,
set_speed_limit=True,
slc_target=45.0 * CV.MPH_TO_MS, slc_target=45.0 * CV.MPH_TO_MS,
slc_offset=3.0 * CV.MPH_TO_MS, slc_offset=3.0 * CV.MPH_TO_MS,
overridden_speed=0.0, overridden_speed=0.0,
@@ -675,6 +635,7 @@ def test_active_slc_control_target_applies_offset_and_cluster_diff():
def test_active_slc_control_target_allows_lower_redneck_override(): def test_active_slc_control_target_allows_lower_redneck_override():
target = get_active_slc_control_target( target = get_active_slc_control_target(
speed_limit_controller=True, speed_limit_controller=True,
set_speed_limit=False,
slc_target=65.0 * CV.MPH_TO_MS, slc_target=65.0 * CV.MPH_TO_MS,
slc_offset=0.0, slc_offset=0.0,
overridden_speed=35.0 * CV.MPH_TO_MS, overridden_speed=35.0 * CV.MPH_TO_MS,
@@ -896,6 +857,65 @@ def test_santa_fe_force_stop_holds_through_low_speed_detector_dropout():
assert result == pytest.approx(0.0) assert result == pytest.approx(0.0)
@pytest.mark.parametrize("fingerprint", ("FORD_MUSTANG_MACH_E_MK1", "FORD_F_150_LIGHTNING_MK1", "OTHER_CAR"))
def test_mach_e_force_stop_completion_is_vehicle_scoped(fingerprint):
planner, vcruise = make_vcruise(red_light=True, forcing_stop=True)
planner.model_length = 60.0
vcruise.force_stop_entry_speed = 12.0
vcruise.tracked_model_length = 4.6
vcruise.force_stop_distance_cap = 4.6
sm = make_sm(standstill=False, car_fingerprint=fingerprint)
sm["modelV2"] = SimpleNamespace(action=SimpleNamespace(shouldStop=False))
toggles = make_toggles()
update_vcruise(vcruise, sm, toggles, now=0.0, v_ego=0.7)
planner.starpilot_cem.stop_light_detected = False
update_vcruise(vcruise, sm, toggles, now=0.25, v_ego=0.7)
result = update_vcruise(vcruise, sm, toggles, now=0.8, v_ego=0.7)
if fingerprint == "FORD_MUSTANG_MACH_E_MK1":
assert get_force_stop_low_speed_hold(sm["carParams"]) == pytest.approx(1.0)
assert vcruise.forcing_stop
assert result == pytest.approx(0.0)
assert vcruise.tracked_model_length <= 4.6
else:
assert get_force_stop_low_speed_hold(sm["carParams"]) is None
assert not vcruise.forcing_stop
assert result == pytest.approx(20.0)
def test_mach_e_force_stop_still_releases_green_above_final_handoff():
planner, vcruise = make_vcruise(red_light=True, forcing_stop=True)
vcruise.force_stop_entry_speed = 12.0
sm = make_sm(standstill=False, car_fingerprint="FORD_MUSTANG_MACH_E_MK1")
toggles = make_toggles()
update_vcruise(vcruise, sm, toggles, now=0.0, v_ego=3.0)
planner.starpilot_cem.stop_light_detected = False
update_vcruise(vcruise, sm, toggles, now=0.25, v_ego=3.0)
result = update_vcruise(vcruise, sm, toggles, now=0.8, v_ego=3.0)
assert not vcruise.forcing_stop
assert result == pytest.approx(20.0)
@pytest.mark.parametrize("override", ("gas", "standstill"))
def test_mach_e_force_stop_completion_does_not_block_departure(override):
planner, vcruise = make_vcruise(red_light=True, forcing_stop=True)
vcruise.force_stop_entry_speed = 12.0
sm = make_sm(standstill=False, car_fingerprint="FORD_MUSTANG_MACH_E_MK1")
toggles = make_toggles()
update_vcruise(vcruise, sm, toggles, now=0.0, v_ego=0.7)
planner.starpilot_cem.stop_light_detected = False
sm["carState"].gasPressed = override == "gas"
sm["carState"].standstill = override == "standstill"
result = update_vcruise(vcruise, sm, toggles, now=0.8, v_ego=0.0 if override == "standstill" else 0.7)
assert not vcruise.forcing_stop
assert result == pytest.approx(20.0)
def test_force_stop_does_not_reanchor_inside_reanchor_floor(): def test_force_stop_does_not_reanchor_inside_reanchor_floor():
planner, vcruise = make_vcruise(red_light=False, raw_model_stopped=False, forcing_stop=True) planner, vcruise = make_vcruise(red_light=False, raw_model_stopped=False, forcing_stop=True)
planner.model_length = 90.0 planner.model_length = 90.0
+2 -1
View File
@@ -664,7 +664,8 @@ class SelfdriveD:
if self.big_model_active and big_failed: if self.big_model_active and big_failed:
self.events.add(EventName.bigModelFailed) self.events.add(EventName.bigModelFailed)
not_running = {p.name for p in self.sm['managerState'].processes if not p.running and p.shouldBeRunning} not_running = {p.name for p in self.sm['managerState'].processes
if not p.running and p.shouldBeRunning and p.name != 'stinger_object_shadow'}
if self.sm.recv_frame['managerState'] and len(not_running): if self.sm.recv_frame['managerState'] and len(not_running):
if not_running != self.not_running_prev: if not_running != self.not_running_prev:
cloudlog.event("process_not_running", not_running=not_running, error=True) cloudlog.event("process_not_running", not_running=not_running, error=True)
@@ -382,8 +382,9 @@ def point_hits(mouse_pos: MousePos, rect: rl.Rectangle, parent_rect: rl.Rectangl
return hit.width > 0 and hit.height > 0 and rl.check_collision_point_rec(mouse_pos, hit) return hit.width > 0 and hit.height > 0 and rl.check_collision_point_rec(mouse_pos, hit)
def wrap_text(font: rl.Font, text: str, max_width: float, font_size: float, max_lines: int = 2) -> list[str]: def wrap_text(font: rl.Font, text: str, max_width: float, font_size: float, max_lines: int = 2,
spacing = font_size * 0.15 *, spacing: float | None = None) -> list[str]:
spacing = font_size * 0.15 if spacing is None else spacing
words = text.split() words = text.split()
lines: list[str] = [] lines: list[str] = []
current = "" current = ""
@@ -1007,13 +1008,16 @@ class PanelManagerView(AetherInteractiveMixin, Widget):
track_h = 10.0 track_h = 10.0
track_w = seg_w * n track_w = seg_w * n
start_x = rect.x + (rect.width - track_w) / 2 start_x = rect.x + (rect.width - track_w) / 2
track_y = rect.y + rect.height - 16 track_y = rect.y + rect.height - 12
label = f"{self._current_page + 1} / {self._page_count}" label = f"{self._current_page + 1} / {self._page_count}"
lf = gui_app.font(FontWeight.MEDIUM) lf = gui_app.font(FontWeight.MEDIUM)
ls = 16.0 ls = 22.0
lw = measure_text_cached(lf, label, int(ls)).x label_size = measure_text_cached(lf, label, int(ls))
rl.draw_text_ex(lf, label, rl.Vector2(int(rect.x + (rect.width - lw) / 2), int(track_y - ls - 6)), int(ls), 0, with_alpha(AetherListColors.MUTED, 200)) rl.draw_text_ex(
lf, label, rl.Vector2(int(rect.x + (rect.width - label_size.x) / 2), int(track_y - label_size.y - 4)),
int(ls), 0, with_alpha(AetherListColors.MUTED, 200),
)
track_col = with_alpha(AetherListColors.MUTED, 60) track_col = with_alpha(AetherListColors.MUTED, 60)
rl.draw_rectangle_rounded(rl.Rectangle(start_x, track_y, track_w, track_h), 0.5, 8, track_col) rl.draw_rectangle_rounded(rl.Rectangle(start_x, track_y, track_w, track_h), 0.5, 8, track_col)
@@ -1025,7 +1029,8 @@ class PanelManagerView(AetherInteractiveMixin, Widget):
if self._page_count > 8: if self._page_count > 8:
more_x = int(start_x + track_w + 10) more_x = int(start_x + track_w + 10)
rl.draw_text_ex(lf, "···", rl.Vector2(more_x, int(track_y - 2)), 14, 0, AetherListColors.MUTED) more_h = measure_text_cached(lf, "···", 14).y
rl.draw_text_ex(lf, "···", rl.Vector2(more_x, int(track_y + track_h - more_h)), 14, 0, AetherListColors.MUTED)
# ── lifecycle ────────────────────────────────────────────── # ── lifecycle ──────────────────────────────────────────────
@@ -1434,11 +1439,23 @@ class BreadcrumbController:
aether_end_scissor_mode() aether_end_scissor_mode()
PANEL_HEADER_TITLE_Y: int = 34 PANEL_HEADER_TITLE_Y: int = 34
PANEL_HEADER_SUBTITLE_Y: int = 78 PANEL_HEADER_SUBTITLE_Y: int = 78
PANEL_HEADER_TITLE_FONT_SIZE: int = 30 PANEL_HEADER_TITLE_FONT_SIZE: int = 44
PANEL_HEADER_SUBTITLE_FONT_SIZE: int = 26 PANEL_HEADER_SUBTITLE_FONT_SIZE: int = 32
PANEL_HEADER_TITLE_FONT: FontWeight = FontWeight.SEMI_BOLD PANEL_HEADER_TITLE_FONT: FontWeight = FontWeight.SEMI_BOLD
PANEL_HEADER_SUBTITLE_FONT: FontWeight = FontWeight.NORMAL PANEL_HEADER_SUBTITLE_FONT: FontWeight = FontWeight.NORMAL
PANEL_HEADER_SUBTITLE_LINE_HEIGHT: float = 30.0 # subtitle_size(26) + interline_gap(4) SETTINGS_ROW_TITLE_FONT_SIZE: int = 40
SETTINGS_ROW_SUBTITLE_FONT_SIZE: int = 28
SETTINGS_ROW_VALUE_FONT_SIZE: int = 34
def _settings_panel_header_layout(width: float, subtitle: str | None, title_size: int, subtitle_size: int,
subtitle_weight: FontWeight = PANEL_HEADER_SUBTITLE_FONT,
min_title_height: float = 0.0) -> tuple[float, list[str], float, float]:
title_height = max(title_size * FONT_SCALE, min_title_height)
line_height = subtitle_size * FONT_SCALE + SPACING.xs
lines = wrap_text(gui_app.font(subtitle_weight), subtitle, width, subtitle_size, max_lines=4, spacing=0) if subtitle else []
height = title_height + (SPACING.sm + len(lines) * line_height - SPACING.xs if lines else 0.0)
return title_height, lines, line_height, height
def draw_settings_panel_header(header_rect: rl.Rectangle, title: str, subtitle: str | None = None, def draw_settings_panel_header(header_rect: rl.Rectangle, title: str, subtitle: str | None = None,
@@ -1450,19 +1467,22 @@ def draw_settings_panel_header(header_rect: rl.Rectangle, title: str, subtitle:
title_color: rl.Color = AetherListColors.HEADER, title_color: rl.Color = AetherListColors.HEADER,
subtitle_color: rl.Color = AetherListColors.SUBTEXT, subtitle_color: rl.Color = AetherListColors.SUBTEXT,
title_weight: FontWeight = PANEL_HEADER_TITLE_FONT, title_weight: FontWeight = PANEL_HEADER_TITLE_FONT,
subtitle_weight: FontWeight = PANEL_HEADER_SUBTITLE_FONT): subtitle_weight: FontWeight = PANEL_HEADER_SUBTITLE_FONT,
min_title_height: float = 0.0):
if not title: if not title:
return return
title_font = gui_app.font(title_weight) title_font = gui_app.font(title_weight)
y = header_rect.y title_height, desc_lines, line_height, _ = _settings_panel_header_layout(
rl.draw_text_ex(title_font, title, rl.Vector2(header_rect.x, y), title_size, 0, title_color) header_rect.width * max_subtitle_width, subtitle, title_size, subtitle_size, subtitle_weight, min_title_height,
y += title_size + 8 )
if subtitle: y = header_rect.y + (title_height - title_size * FONT_SCALE) / 2
draw_text_fit_common(title_font, title, rl.Vector2(header_rect.x, y), header_rect.width * max_title_width, title_size, color=title_color)
y = header_rect.y + title_height + SPACING.sm
if desc_lines:
desc_font = gui_app.font(subtitle_weight) desc_font = gui_app.font(subtitle_weight)
desc_lines = wrap_text(desc_font, subtitle, header_rect.width * max_subtitle_width, subtitle_size, max_lines=4)
for line in desc_lines: for line in desc_lines:
rl.draw_text_ex(desc_font, line, rl.Vector2(header_rect.x, y), subtitle_size, 0, subtitle_color) draw_text_fit_common(desc_font, line, rl.Vector2(header_rect.x, y), header_rect.width * max_subtitle_width, subtitle_size, color=subtitle_color)
y += subtitle_size + 4 y += line_height
@@ -1590,7 +1610,6 @@ def draw_standard_toggle_row(
pressed=pressed, pressed=pressed,
is_last=is_last, is_last=is_last,
show_chevron=False, show_chevron=False,
title_size=36, subtitle_size=26,
style=style, style=style,
) )
@@ -1783,7 +1802,7 @@ def draw_action_pill(
draw_text_fit_common( draw_text_fit_common(
gui_app.font(FontWeight.SEMI_BOLD), gui_app.font(FontWeight.SEMI_BOLD),
text, text,
rl.Vector2(rect.x + 12, rect.y + (rect.height - font_size) / 2), rl.Vector2(rect.x + 12, rect.y + (rect.height - font_size * FONT_SCALE) / 2),
max(1.0, rect.width - 24), max(1.0, rect.width - 24),
font_size, font_size,
align_center=True, align_center=True,
@@ -2099,9 +2118,9 @@ def draw_settings_list_row(
pressed: bool = False, pressed: bool = False,
is_last: bool = False, is_last: bool = False,
show_chevron: bool = True, show_chevron: bool = True,
title_size: int = 36, title_size: int = SETTINGS_ROW_TITLE_FONT_SIZE,
subtitle_size: int = 26, subtitle_size: int = SETTINGS_ROW_SUBTITLE_FONT_SIZE,
value_size: int = 28, value_size: int = SETTINGS_ROW_VALUE_FONT_SIZE,
separator_inset: int = 24, separator_inset: int = 24,
title_color: rl.Color | None = None, title_color: rl.Color | None = None,
subtitle_color: rl.Color | None = None, subtitle_color: rl.Color | None = None,
@@ -2139,9 +2158,9 @@ def draw_settings_list_row(
text_width = max(100.0, text_right - text_left) text_width = max(100.0, text_right - text_left)
if subtitle: if subtitle:
eff_title_size = min(36, title_size) eff_title_size = min(SETTINGS_ROW_TITLE_FONT_SIZE, title_size)
eff_sub_size = min(26, subtitle_size) eff_sub_size = min(SETTINGS_ROW_SUBTITLE_FONT_SIZE, subtitle_size)
total_h = eff_title_size + eff_sub_size + 4 total_h = (eff_title_size + eff_sub_size) * FONT_SCALE + SPACING.xs
start_y = draw_rect.y + (draw_rect.height - total_h) / 2 start_y = draw_rect.y + (draw_rect.height - total_h) / 2
draw_text_fit_common( draw_text_fit_common(
@@ -2152,13 +2171,13 @@ def draw_settings_list_row(
) )
draw_text_fit_common( draw_text_fit_common(
gui_app.font(FontWeight.NORMAL), subtitle, gui_app.font(FontWeight.NORMAL), subtitle,
rl.Vector2(text_left, start_y + eff_title_size + 4), rl.Vector2(text_left, start_y + eff_title_size * FONT_SCALE + SPACING.xs),
text_width, eff_sub_size, text_width, eff_sub_size,
color=resolved_subtitle_color, color=resolved_subtitle_color,
) )
else: else:
eff_title_size = min(36, title_size) eff_title_size = min(SETTINGS_ROW_TITLE_FONT_SIZE, title_size)
title_y = draw_rect.y + (draw_rect.height - eff_title_size) / 2 title_y = draw_rect.y + (draw_rect.height - eff_title_size * FONT_SCALE) / 2
draw_text_fit_common( draw_text_fit_common(
gui_app.font(FontWeight.SEMI_BOLD), title, gui_app.font(FontWeight.SEMI_BOLD), title,
rl.Vector2(text_left, title_y), rl.Vector2(text_left, title_y),
@@ -2188,11 +2207,11 @@ def draw_settings_list_row(
if value: if value:
if is_narrow and draw_rect.height >= 86 and not subtitle: if is_narrow and draw_rect.height >= 86 and not subtitle:
# Adaptive Two-Line Stacked Layout: Title on top, Value spanning full width below # Adaptive Two-Line Stacked Layout: Title on top, Value spanning full width below
eff_title_size = min(34, title_size) eff_title_size = min(SETTINGS_ROW_TITLE_FONT_SIZE, title_size)
eff_value_size = min(28, value_size) eff_value_size = min(SETTINGS_ROW_VALUE_FONT_SIZE, value_size)
available_w = max(100.0, draw_rect.width - 48 - (32 if show_chevron else 0)) available_w = max(100.0, draw_rect.width - 48 - (32 if show_chevron else 0))
total_h = eff_title_size + eff_value_size + 6 total_h = (eff_title_size + eff_value_size) * FONT_SCALE + 6
start_y = draw_rect.y + (draw_rect.height - total_h) / 2 start_y = draw_rect.y + (draw_rect.height - total_h) / 2
draw_text_fit_common( draw_text_fit_common(
@@ -2203,7 +2222,7 @@ def draw_settings_list_row(
) )
draw_text_fit_common( draw_text_fit_common(
gui_app.font(FontWeight.MEDIUM), value, gui_app.font(FontWeight.MEDIUM), value,
rl.Vector2(text_left, start_y + eff_title_size + 6), rl.Vector2(text_left, start_y + eff_title_size * FONT_SCALE + 6),
available_w, eff_value_size, available_w, eff_value_size,
color=resolved_subtitle_color if resolved_value_color == resolved_title_color else resolved_value_color, color=resolved_subtitle_color if resolved_value_color == resolved_title_color else resolved_value_color,
) )
@@ -2219,13 +2238,13 @@ def draw_settings_list_row(
t_width = max(100.0, draw_rect.width - 48 - v_width - (32 if show_chevron else 0)) t_width = max(100.0, draw_rect.width - 48 - v_width - (32 if show_chevron else 0))
v_right = chevron_rect.x - 16 if show_chevron else draw_rect.x + draw_rect.width - 24 v_right = chevron_rect.x - 16 if show_chevron else draw_rect.x + draw_rect.width - 24
eff_value_size = min(28, value_size) if is_narrow else min(32, value_size) eff_value_size = min(SETTINGS_ROW_VALUE_FONT_SIZE, value_size)
value_y = draw_rect.y + (draw_rect.height - eff_value_size) / 2 value_y = draw_rect.y + (draw_rect.height - eff_value_size * FONT_SCALE) / 2
if subtitle: if subtitle:
eff_title_size = min(34, title_size) eff_title_size = min(SETTINGS_ROW_TITLE_FONT_SIZE, title_size)
eff_sub_size = min(26, subtitle_size) eff_sub_size = min(SETTINGS_ROW_SUBTITLE_FONT_SIZE, subtitle_size)
total_h = eff_title_size + eff_sub_size + 4 total_h = (eff_title_size + eff_sub_size) * FONT_SCALE + SPACING.xs
start_y = draw_rect.y + (draw_rect.height - total_h) / 2 start_y = draw_rect.y + (draw_rect.height - total_h) / 2
draw_text_fit_common( draw_text_fit_common(
@@ -2236,13 +2255,13 @@ def draw_settings_list_row(
) )
draw_text_fit_common( draw_text_fit_common(
gui_app.font(FontWeight.NORMAL), subtitle, gui_app.font(FontWeight.NORMAL), subtitle,
rl.Vector2(text_left, start_y + eff_title_size + 4), rl.Vector2(text_left, start_y + eff_title_size * FONT_SCALE + SPACING.xs),
t_width, eff_sub_size, t_width, eff_sub_size,
color=resolved_subtitle_color, color=resolved_subtitle_color,
) )
else: else:
eff_title_size = min(36, title_size) if is_narrow else title_size eff_title_size = min(SETTINGS_ROW_TITLE_FONT_SIZE, title_size) if is_narrow else title_size
title_y = draw_rect.y + (draw_rect.height - eff_title_size) / 2 title_y = draw_rect.y + (draw_rect.height - eff_title_size * FONT_SCALE) / 2
draw_text_fit_common( draw_text_fit_common(
gui_app.font(FontWeight.SEMI_BOLD), title, gui_app.font(FontWeight.SEMI_BOLD), title,
@@ -2267,9 +2286,9 @@ def draw_settings_list_row(
text_right = chevron_rect.x - 12 if show_chevron else draw_rect.x + draw_rect.width - 24 text_right = chevron_rect.x - 12 if show_chevron else draw_rect.x + draw_rect.width - 24
text_width = max(100.0, text_right - text_left) text_width = max(100.0, text_right - text_left)
if subtitle: if subtitle:
eff_title_size = min(36, title_size) eff_title_size = min(SETTINGS_ROW_TITLE_FONT_SIZE, title_size)
eff_sub_size = min(26, subtitle_size) eff_sub_size = min(SETTINGS_ROW_SUBTITLE_FONT_SIZE, subtitle_size)
total_h = eff_title_size + eff_sub_size + 4 total_h = (eff_title_size + eff_sub_size) * FONT_SCALE + SPACING.xs
start_y = draw_rect.y + (draw_rect.height - total_h) / 2 start_y = draw_rect.y + (draw_rect.height - total_h) / 2
draw_text_fit_common( draw_text_fit_common(
gui_app.font(FontWeight.SEMI_BOLD), title, gui_app.font(FontWeight.SEMI_BOLD), title,
@@ -2279,13 +2298,13 @@ def draw_settings_list_row(
) )
draw_text_fit_common( draw_text_fit_common(
gui_app.font(FontWeight.NORMAL), subtitle, gui_app.font(FontWeight.NORMAL), subtitle,
rl.Vector2(text_left, start_y + eff_title_size + 4), rl.Vector2(text_left, start_y + eff_title_size * FONT_SCALE + SPACING.xs),
text_width, eff_sub_size, text_width, eff_sub_size,
color=resolved_subtitle_color, color=resolved_subtitle_color,
) )
else: else:
eff_title_size = min(36, title_size) eff_title_size = min(SETTINGS_ROW_TITLE_FONT_SIZE, title_size)
title_y = draw_rect.y + (draw_rect.height - eff_title_size) / 2 title_y = draw_rect.y + (draw_rect.height - eff_title_size * FONT_SCALE) / 2
draw_text_fit_common( draw_text_fit_common(
gui_app.font(FontWeight.SEMI_BOLD), title, gui_app.font(FontWeight.SEMI_BOLD), title,
rl.Vector2(text_left, title_y), rl.Vector2(text_left, title_y),
@@ -2323,7 +2342,7 @@ def draw_selectable_chip(rect: rl.Rectangle, text: str, *,
draw_text_fit_common( draw_text_fit_common(
resolved_font, resolved_font,
text, text,
rl.Vector2(rect.x + padding_x, rect.y + (rect.height - font_size) / 2), rl.Vector2(rect.x + padding_x, rect.y + (rect.height - font_size * FONT_SCALE) / 2),
max(1.0, rect.width - padding_x * 2), max(1.0, rect.width - padding_x * 2),
font_size, font_size,
align_center=True, align_center=True,
@@ -2846,7 +2865,7 @@ class AetherAdjustorRow(Widget):
draw_rounded_fill(fill_rect, with_alpha(self._color, fill_alpha), radius_px=bar_h // 2) draw_rounded_fill(fill_rect, with_alpha(self._color, fill_alpha), radius_px=bar_h // 2)
inset = 18 inset = 18
title_y = bar_rect.y + (bar_h - title_fs) / 2 title_y = bar_rect.y + (bar_h - title_fs * FONT_SCALE) / 2
rl.draw_text_ex(self._font_title, self._title, rl.draw_text_ex(self._font_title, self._title,
rl.Vector2(bar_rect.x + inset, title_y), rl.Vector2(bar_rect.x + inset, title_y),
title_fs, 0, self._style.title_color) title_fs, 0, self._style.title_color)
@@ -2855,7 +2874,7 @@ class AetherAdjustorRow(Widget):
value_w = measure_text_cached(self._font_value, value_str, value_fs).x value_w = measure_text_cached(self._font_value, value_str, value_fs).x
rl.draw_text_ex(self._font_value, value_str, rl.draw_text_ex(self._font_value, value_str,
rl.Vector2(bar_rect.x + bar_rect.width - inset - value_w, rl.Vector2(bar_rect.x + bar_rect.width - inset - value_w,
bar_rect.y + (bar_h - value_fs) / 2), bar_rect.y + (bar_h - value_fs * FONT_SCALE) / 2),
value_fs, 0, self._style.title_color) value_fs, 0, self._style.title_color)
if self._subtitle: if self._subtitle:
@@ -2950,11 +2969,11 @@ def draw_selection_list_row(
subtitle_font = gui_app.font(FontWeight.NORMAL) subtitle_font = gui_app.font(FontWeight.NORMAL)
if subtitle: if subtitle:
text_height = title_size + subtitle_size + 8 text_height = (title_size + subtitle_size) * FONT_SCALE + 8
title_y = info_rect.y + (info_rect.height - text_height) / 2 title_y = info_rect.y + (info_rect.height - text_height) / 2
subtitle_y = title_y + title_size + 8 subtitle_y = title_y + title_size * FONT_SCALE + 8
else: else:
title_y = info_rect.y + (info_rect.height - title_size) / 2 title_y = info_rect.y + (info_rect.height - title_size * FONT_SCALE) / 2
subtitle_y = title_y subtitle_y = title_y
draw_text_fit_common( draw_text_fit_common(
@@ -3112,7 +3131,7 @@ class AetherButton(Widget):
draw_text_fit_common( draw_text_fit_common(
gui_app.font(FontWeight.MEDIUM), gui_app.font(FontWeight.MEDIUM),
self.text, self.text,
rl.Vector2(rect.x + 18, rect.y + (rect.height - self._font_size) / 2), rl.Vector2(rect.x + 18, rect.y + (rect.height - self._font_size * FONT_SCALE) / 2),
max(1.0, rect.width - 36), max(1.0, rect.width - 36),
self._font_size, self._font_size,
align_center=True, align_center=True,
@@ -3265,7 +3284,7 @@ class AetherSettingsView(PanelManagerView):
if self._parent_toggle: if self._parent_toggle:
mid = f"parent_toggle:{self._parent_toggle.label}" mid = f"parent_toggle:{self._parent_toggle.label}"
rect = self._interactive_rects.get(mid) rect = self._interactive_rects.get(mid)
if rect and point_hits(mouse_pos, rect, None, pad_x=6, pad_y=6): if rect and point_hits(mouse_pos, rect, None, pad_x=0, pad_y=0):
return mid return mid
return super()._target_at(mouse_pos) return super()._target_at(mouse_pos)
@@ -3290,32 +3309,29 @@ class AetherSettingsView(PanelManagerView):
elif row.type == "toggle" and row.set_state and row.get_state: elif row.type == "toggle" and row.set_state and row.get_state:
row.set_state(not row.get_state()) row.set_state(not row.get_state())
def _header_text(self) -> tuple[str, str]:
title, subtitle = self._header_title, self._header_subtitle
if self._parent_toggle:
title = title or self._parent_toggle.label
subtitle = subtitle or self._parent_toggle.subtitle
return tr(title), tr(subtitle) if subtitle else ""
def _header_text_width(self, width: float) -> float:
right_inset = SPACING.xl
if self._parent_toggle:
right_inset = AETHER_LIST_METRICS.toggle_width + AETHER_LIST_METRICS.toggle_right_inset + SPACING.lg
return max(100.0, width - SPACING.xl - right_inset)
def _compute_header_height(self, content_width: float) -> float: def _compute_header_height(self, content_width: float) -> float:
if not self._has_header: if not self._has_header:
return 0.0 return 0.0
if self._parent_toggle: _, subtitle = self._header_text()
h = max(float(AETHER_LIST_METRICS.toggle_height), 54.0) # toggle vs title(46px + 8px gap) _, _, _, height = _settings_panel_header_layout(
subtitle_text = tr(self._parent_toggle.subtitle) if self._parent_toggle.subtitle else "" self._header_text_width(content_width + AETHER_LIST_METRICS.content_right_gutter), subtitle,
if self._header_subtitle: PANEL_HEADER_TITLE_FONT_SIZE, PANEL_HEADER_SUBTITLE_FONT_SIZE,
subtitle_text = tr(self._header_subtitle) min_title_height=AETHER_LIST_METRICS.toggle_height if self._parent_toggle else 0.0,
if subtitle_text: )
toggle_take = AETHER_LIST_METRICS.toggle_width + AETHER_LIST_METRICS.toggle_right_inset + 16 return height + SECTION_GAP
col_w = max(100.0, content_width + AETHER_LIST_METRICS.content_right_gutter - toggle_take)
desc_font = gui_app.font(FontWeight.NORMAL)
desc_lines = wrap_text(desc_font, subtitle_text, col_w, 29, max_lines=4)
h += len(desc_lines) * PANEL_HEADER_SUBTITLE_LINE_HEIGHT + 12.0
h += SECTION_GAP
return h
h = 54.0 # title (46px) + inner gap (8px)
if self._header_subtitle:
subtitle_text = tr(self._header_subtitle)
if subtitle_text:
desc_font = gui_app.font(FontWeight.NORMAL)
col_w = (content_width - self.COLUMN_GAP) / 2 if self._uses_two_columns(content_width) else content_width
desc_lines = wrap_text(desc_font, subtitle_text, col_w, 29, max_lines=4)
h += len(desc_lines) * PANEL_HEADER_SUBTITLE_LINE_HEIGHT + 12.0
h += SECTION_GAP
return h
def _render(self, rect: rl.Rectangle): def _render(self, rect: rl.Rectangle):
self.set_rect(rect) self.set_rect(rect)
@@ -3357,25 +3373,18 @@ class AetherSettingsView(PanelManagerView):
AetherListColors.PANEL_BG, fade_height=self._fade_height) AetherListColors.PANEL_BG, fade_height=self._fade_height)
def _draw_header(self, rect: rl.Rectangle): def _draw_header(self, rect: rl.Rectangle):
title = tr(self._header_title) if self._header_title else "" title, subtitle = self._header_text()
subtitle = tr(self._header_subtitle) if self._header_subtitle else "" text_rect = rl.Rectangle(rect.x + SPACING.xl, rect.y, self._header_text_width(rect.width), rect.height)
draw_settings_panel_header(
text_rect, title, subtitle, max_title_width=1.0, max_subtitle_width=1.0,
min_title_height=AETHER_LIST_METRICS.toggle_height if self._parent_toggle else 0.0,
)
if self._parent_toggle: if self._parent_toggle:
toggle = self._parent_toggle toggle = self._parent_toggle
display_title = title if title else tr(toggle.label)
subtitle_text = subtitle if subtitle else (tr(toggle.subtitle) if toggle.subtitle else "")
toggle_take = AETHER_LIST_METRICS.toggle_width + AETHER_LIST_METRICS.toggle_right_inset + 16
text_rect = rl.Rectangle(rect.x, rect.y, max(100.0, rect.width - toggle_take), rect.height)
draw_settings_panel_header(text_rect, display_title, subtitle_text, title_size=30, subtitle_size=26, max_subtitle_width=1.0)
toggle_id = f"parent_toggle:{toggle.label}" toggle_id = f"parent_toggle:{toggle.label}"
tw = AETHER_LIST_METRICS.toggle_width
th = AETHER_LIST_METRICS.toggle_height th = AETHER_LIST_METRICS.toggle_height
ri = AETHER_LIST_METRICS.toggle_right_inset self._interactive_rects[toggle_id] = rl.Rectangle(rect.x, rect.y, rect.width, rect.height - SECTION_GAP)
toggle_rect = rl.Rectangle(rect.x + rect.width - tw - ri, rect.y, tw, th)
self._interactive_rects[toggle_id] = toggle_rect
toggle_value = toggle.get_state() toggle_value = toggle.get_state()
@@ -3388,8 +3397,6 @@ class AetherSettingsView(PanelManagerView):
radius_px=100, radius_px=100,
bg_color=rl.Color(12, 10, 18, 255), bg_color=rl.Color(12, 10, 18, 255),
) )
else:
draw_settings_panel_header(rect, title, subtitle, title_size=30, subtitle_size=26)
def _active_sections(self) -> list[SettingSection]: def _active_sections(self) -> list[SettingSection]:
if self._tab_defs and self._active_tab_key: if self._tab_defs and self._active_tab_key:
@@ -3483,11 +3490,11 @@ class AetherSettingsView(PanelManagerView):
group_h = max(section_h, right_h) group_h = max(section_h, right_h)
draw_section_header( draw_section_header(
rl.Rectangle(rect.x, y, col_w, SECTION_HEADER_HEIGHT), rl.Rectangle(rect.x + SPACING.xl, y, col_w - SPACING.xl * 2, SECTION_HEADER_HEIGHT),
tr(section.title), style=self._panel_style, tr(section.title), style=self._panel_style,
) )
draw_section_header( draw_section_header(
rl.Rectangle(rect.x + col_w + self.COLUMN_GAP, y, col_w, SECTION_HEADER_HEIGHT), rl.Rectangle(rect.x + col_w + self.COLUMN_GAP + SPACING.xl, y, col_w - SPACING.xl * 2, SECTION_HEADER_HEIGHT),
tr(right_section.title), style=self._panel_style, tr(right_section.title), style=self._panel_style,
) )
y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP
@@ -3514,7 +3521,7 @@ class AetherSettingsView(PanelManagerView):
section: SettingSection, rows: list[SettingRow]) -> float: section: SettingSection, rows: list[SettingRow]) -> float:
if section.title: if section.title:
draw_section_header( draw_section_header(
rl.Rectangle(x, y, width, SECTION_HEADER_HEIGHT), rl.Rectangle(x + SPACING.xl, y, width - SPACING.xl * 2, SECTION_HEADER_HEIGHT),
tr(section.title), tr(section.title),
style=self._panel_style, style=self._panel_style,
) )
@@ -3555,7 +3562,6 @@ class AetherSettingsView(PanelManagerView):
pressed=pressed, pressed=pressed,
is_last=is_last, is_last=is_last,
show_chevron=row.on_click is not None, show_chevron=row.on_click is not None,
title_size=36, subtitle_size=26, value_size=30,
style=self._panel_style, style=self._panel_style,
) )
elif row.type == "action": elif row.type == "action":
@@ -3571,7 +3577,7 @@ class AetherSettingsView(PanelManagerView):
pressed=pressed, pressed=pressed,
is_last=is_last, is_last=is_last,
action_pill=True, action_pill=True,
title_size=36, subtitle_size=26, title_size=SETTINGS_ROW_TITLE_FONT_SIZE, subtitle_size=SETTINGS_ROW_SUBTITLE_FONT_SIZE,
action_pill_height=AETHER_LIST_METRICS.toggle_height, action_text_size=26, action_pill_height=AETHER_LIST_METRICS.toggle_height, action_text_size=26,
action_text_color=action_text_color, action_text_color=action_text_color,
action_fill=action_fill, action_fill=action_fill,
@@ -3887,8 +3893,8 @@ class AetherTile(Widget):
title_color = rl.WHITE if (enabled and is_active) else rl.Color(236, 242, 250, 255) title_color = rl.WHITE if (enabled and is_active) else rl.Color(236, 242, 250, 255)
title_y = ry + (rh / 2) - title_size - 2 title_y = ry + (rh - (title_size + status_size) * FONT_SCALE - 8) / 2
status_y = ry + (rh / 2) + 6 status_y = title_y + title_size * FONT_SCALE + 8
max_text_width = rw - (content_pad * 2) - int(rh * 0.40) - 10 max_text_width = rw - (content_pad * 2) - int(rh * 0.40) - 10
font = getattr(self, "_font", gui_app.font(FontWeight.MEDIUM)) font = getattr(self, "_font", gui_app.font(FontWeight.MEDIUM))
@@ -5410,7 +5416,7 @@ class AetherSegmentedControl(Widget):
draw_text_fit_common( draw_text_fit_common(
self._font, self._font,
label, label,
rl.Vector2(face_rect.x + 16, face_rect.y + (face_rect.height - title_size) / 2), rl.Vector2(face_rect.x + 16, face_rect.y + (face_rect.height - title_size * FONT_SCALE) / 2),
face_rect.width - 32, face_rect.width - 32,
title_size, title_size,
align_center=True, align_center=True,
@@ -26,7 +26,7 @@ from openpilot.starpilot.assets.model_manager import (
set_model_profile, set_model_profile,
) )
from openpilot.starpilot.common.starpilot_variables import MODELS_PATH, update_starpilot_toggles from openpilot.starpilot.common.starpilot_variables import MODELS_PATH, update_starpilot_toggles
from openpilot.system.ui.lib.application import FontWeight, MouseEvent, MousePos, gui_app from openpilot.system.ui.lib.application import FONT_SCALE, FontWeight, MouseEvent, MousePos, gui_app
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2
from openpilot.system.ui.widgets import DialogResult, Widget from openpilot.system.ui.widgets import DialogResult, Widget
@@ -63,6 +63,9 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
SECTION_HEADER_HEIGHT, SECTION_HEADER_HEIGHT,
SECTION_HEADER_GAP, SECTION_HEADER_GAP,
ROW_HEIGHT, ROW_HEIGHT,
SETTINGS_ROW_TITLE_FONT_SIZE,
SETTINGS_ROW_SUBTITLE_FONT_SIZE,
SPACING,
) )
ROW_RADIUS = AETHER_LIST_METRICS.row_radius ROW_RADIUS = AETHER_LIST_METRICS.row_radius
ACTION_WIDTH = AETHER_LIST_METRICS.action_width ACTION_WIDTH = AETHER_LIST_METRICS.action_width
@@ -74,10 +77,10 @@ TRANSITION_SECONDS = 0.24
PANEL_STYLE = DEFAULT_PANEL_STYLE PANEL_STYLE = DEFAULT_PANEL_STYLE
BANNER_HEIGHT = 128.0 BANNER_HEIGHT = 128.0
BANNER_GAP = 14.0 BANNER_GAP = 14.0
HEADER_BUTTON_HEIGHT = 80.0 HEADER_BUTTON_HEIGHT = float(ROW_HEIGHT)
HEADER_BUTTON_GAP_Y = 14.0 HEADER_BUTTON_GAP_Y = 14.0
MANAGEMENT_STRIP_HEIGHT = 64.0 MANAGEMENT_STRIP_HEIGHT = float(ROW_HEIGHT)
MANAGEMENT_PILL_HEIGHT = 44.0 MANAGEMENT_PILL_HEIGHT = float(AETHER_LIST_METRICS.toggle_height)
EMPTY_STATE_HEIGHT = 240.0 EMPTY_STATE_HEIGHT = 240.0
_SORT_MODES = ("alphabetical", "date", "date_oldest", "favorites", "community_picks") _SORT_MODES = ("alphabetical", "date", "date_oldest", "favorites", "community_picks")
_SORT_LABELS = { _SORT_LABELS = {
@@ -136,6 +139,7 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
lambda: self._controller.cancel_active_download() if self._controller._is_download_active() else self._controller.download_all_missing(), lambda: self._controller.cancel_active_download() if self._controller._is_download_active() else self._controller.download_all_missing(),
enabled=lambda: self._controller.primary_header_button_state()[1], enabled=lambda: self._controller.primary_header_button_state()[1],
emphasized=True, emphasized=True,
font_size=SETTINGS_ROW_TITLE_FONT_SIZE,
accent_color=rl.Color(139, 92, 246, 92), accent_color=rl.Color(139, 92, 246, 92),
) )
) )
@@ -145,6 +149,7 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
self._controller.refresh_manifest, self._controller.refresh_manifest,
enabled=lambda: self._controller.secondary_header_button_state()[1], enabled=lambda: self._controller.secondary_header_button_state()[1],
emphasized=False, emphasized=False,
font_size=SETTINGS_ROW_TITLE_FONT_SIZE,
) )
) )
self._random_model_button = self._child( self._random_model_button = self._child(
@@ -152,7 +157,7 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
lambda: self._controller.random_model_button_label(), lambda: self._controller.random_model_button_label(),
self._controller.toggle_model_randomizer, self._controller.toggle_model_randomizer,
emphasized=False, emphasized=False,
font_size=28, font_size=SETTINGS_ROW_TITLE_FONT_SIZE,
) )
) )
self._primary_header_button.set_touch_valid_callback(lambda: self._scroll_panel.is_touch_valid()) self._primary_header_button.set_touch_valid_callback(lambda: self._scroll_panel.is_touch_valid())
@@ -212,12 +217,11 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
for prefix in ("menu:", "action:", "row:"): for prefix in ("menu:", "action:", "row:"):
for target_id, rect in self._interactive_rects.items(): for target_id, rect in self._interactive_rects.items():
if target_id.startswith(prefix): if target_id.startswith(prefix):
pad_y = 6 if prefix == "menu:" else 0 if point_hits(mouse_pos, rect, self._scroll_rect, pad_x=0, pad_y=0):
if point_hits(mouse_pos, rect, self._scroll_rect, pad_x=6, pad_y=pad_y):
return target_id return target_id
for target_id, rect in self._interactive_rects.items(): for target_id, rect in self._interactive_rects.items():
if target_id.startswith("sortopt:") or target_id.startswith("mgmt:"): if target_id.startswith("sortopt:") or target_id.startswith("mgmt:"):
if point_hits(mouse_pos, rect, self._shell_rect, pad_x=6, pad_y=6): if point_hits(mouse_pos, rect, self._shell_rect, pad_x=0, pad_y=0):
return target_id return target_id
return None return None
@@ -365,7 +369,7 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
pill_h = MANAGEMENT_PILL_HEIGHT pill_h = MANAGEMENT_PILL_HEIGHT
pill_y = y + (MANAGEMENT_STRIP_HEIGHT - pill_h) / 2 pill_y = y + (MANAGEMENT_STRIP_HEIGHT - pill_h) / 2
left = x + 16 left = x + 16
gap = 8.0 gap = float(SPACING.lg)
usable = width - 32.0 usable = width - 32.0
if randomizer_on: if randomizer_on:
@@ -377,14 +381,14 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
draw_action_pill(bl_pill, bl_label, draw_action_pill(bl_pill, bl_label,
with_alpha(AetherListColors.PRIMARY, 18), with_alpha(AetherListColors.PRIMARY, 18),
with_alpha(AetherListColors.PRIMARY, 50), with_alpha(AetherListColors.PRIMARY, 50),
AetherListColors.HEADER, font_size=28, roundness=0.35) AetherListColors.HEADER, font_size=30, roundness=0.35)
self._interactive_rects["mgmt:blacklist"] = bl_pill self._interactive_rects["mgmt:blacklist"] = rl.Rectangle(bl_pill.x, y, bl_w, MANAGEMENT_STRIP_HEIGHT)
rt_pill = rl.Rectangle(left + bl_w + gap, pill_y, rt_w, pill_h) rt_pill = rl.Rectangle(left + bl_w + gap, pill_y, rt_w, pill_h)
draw_action_pill(rt_pill, tr("Ratings"), draw_action_pill(rt_pill, tr("Ratings"),
with_alpha(AetherListColors.PRIMARY, 18), with_alpha(AetherListColors.PRIMARY, 18),
with_alpha(AetherListColors.PRIMARY, 50), with_alpha(AetherListColors.PRIMARY, 50),
AetherListColors.HEADER, font_size=28, roundness=0.35) AetherListColors.HEADER, font_size=30, roundness=0.35)
self._interactive_rects["mgmt:ratings"] = rt_pill self._interactive_rects["mgmt:ratings"] = rl.Rectangle(rt_pill.x, y, rt_w, MANAGEMENT_STRIP_HEIGHT)
return return
sort_mode = self._controller._get_sort_mode() sort_mode = self._controller._get_sort_mode()
@@ -410,8 +414,8 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
else: else:
fill = rl.Color(255, 255, 255, 8) fill = rl.Color(255, 255, 255, 8)
border = with_alpha(AetherListColors.PRIMARY, 80) if is_active else rl.Color(255, 255, 255, 24) border = with_alpha(AetherListColors.PRIMARY, 80) if is_active else rl.Color(255, 255, 255, 24)
draw_action_pill(seg_rect, label, fill, border, AetherListColors.HEADER, font_size=28, roundness=0.3) draw_action_pill(seg_rect, label, fill, border, AetherListColors.HEADER, font_size=30, roundness=0.3)
self._interactive_rects[f"sortopt:{mode}"] = seg_rect self._interactive_rects[f"sortopt:{mode}"] = rl.Rectangle(seg_x, y, seg_w, MANAGEMENT_STRIP_HEIGHT)
def _get_sections(self) -> list[tuple[str, list[ModelCatalogEntry]]]: def _get_sections(self) -> list[tuple[str, list[ModelCatalogEntry]]]:
sort_mode = self._controller._get_sort_mode() sort_mode = self._controller._get_sort_mode()
@@ -479,7 +483,7 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
) )
def _draw_model_section(self, x: float, y: float, width: float, title: str, entries: list[ModelCatalogEntry]) -> float: def _draw_model_section(self, x: float, y: float, width: float, title: str, entries: list[ModelCatalogEntry]) -> float:
draw_section_header(rl.Rectangle(x, y, width, SECTION_HEADER_HEIGHT), title, style=PANEL_STYLE) draw_section_header(rl.Rectangle(x + SPACING.xl, y, width - SPACING.xl * 2, SECTION_HEADER_HEIGHT), title, style=PANEL_STYLE)
y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP
group_rect = rl.Rectangle(x, y, width, len(entries) * ROW_HEIGHT) group_rect = rl.Rectangle(x, y, width, len(entries) * ROW_HEIGHT)
@@ -522,12 +526,16 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
separator_inset=22, separator_inset=22,
) )
action_rect = draw_action_rail(draw_rect, ACTION_WIDTH, current=current, alpha=alpha, fill=AetherListColors.ACTION_BG, separator=AetherListColors.ACTION_SEPARATOR, inset_y=18) action_width = ACTION_WIDTH * 2 + SPACING.lg if is_menu_open else ACTION_WIDTH
action_rect = draw_action_rail(
draw_rect, action_width, current=current, alpha=alpha,
fill=AetherListColors.ACTION_BG, separator=AetherListColors.ACTION_SEPARATOR, inset_y=18,
)
info_rect = rl.Rectangle(draw_rect.x + 24, draw_rect.y + 18, draw_rect.width - ACTION_WIDTH - 42, draw_rect.height - 36) info_rect = rl.Rectangle(draw_rect.x + 24, draw_rect.y + 18, draw_rect.width - action_width - 42, draw_rect.height - 36)
row_touchable = entry.installed and not self._controller._params.get_bool("ModelRandomizer") row_touchable = entry.installed and not self._controller._params.get_bool("ModelRandomizer")
if row_touchable: if row_touchable:
self._interactive_rects[f"row:{entry.key}"] = draw_rect self._interactive_rects[f"row:{entry.key}"] = rl.Rectangle(draw_rect.x, draw_rect.y, draw_rect.width - action_width, draw_rect.height)
self._draw_model_info(info_rect, entry, current) self._draw_model_info(info_rect, entry, current)
@@ -538,7 +546,8 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
elif not removable: elif not removable:
self._draw_protected_action(action_rect) self._draw_protected_action(action_rect)
else: else:
self._interactive_rects[f"action:{entry.key}"] = action_rect if not is_menu_open:
self._interactive_rects[f"action:{entry.key}"] = action_rect
self._draw_menu_action(action_rect, is_menu_open, entry) self._draw_menu_action(action_rect, is_menu_open, entry)
else: else:
self._interactive_rects[f"action:{entry.key}"] = action_rect self._interactive_rects[f"action:{entry.key}"] = action_rect
@@ -548,20 +557,20 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
self._draw_download_action(action_rect) self._draw_download_action(action_rect)
def _draw_model_info(self, rect: rl.Rectangle, entry: ModelCatalogEntry, current: bool): def _draw_model_info(self, rect: rl.Rectangle, entry: ModelCatalogEntry, current: bool):
title_size = 36 title_size = SETTINGS_ROW_TITLE_FONT_SIZE
meta_size = 26 meta_size = SETTINGS_ROW_SUBTITLE_FONT_SIZE
inter_gap = 6 inter_gap = 6
total_h = title_size + meta_size + inter_gap total_h = (title_size + meta_size) * FONT_SCALE + inter_gap
start_y = rect.y + (rect.height - total_h) / 2 start_y = rect.y + (rect.height - total_h) / 2
heart_offset = 0 heart_offset = 0
if entry.user_favorite: if entry.user_favorite:
heart_color = rl.Color(210, 100, 130, 230) heart_color = rl.Color(210, 100, 130, 230)
heart_center = rl.Vector2(rect.x + 14, start_y + title_size / 2) heart_center = rl.Vector2(rect.x + 14, start_y + title_size * FONT_SCALE / 2)
draw_heart_icon(heart_center, heart_color) draw_heart_icon(heart_center, heart_color)
heart_offset = 36 heart_offset = 36
title_rect = rl.Rectangle(rect.x + heart_offset, start_y, rect.width - heart_offset, title_size) title_rect = rl.Rectangle(rect.x + heart_offset, start_y, rect.width - heart_offset, title_size * FONT_SCALE)
gui_label(title_rect, entry.name, title_size, AetherListColors.HEADER, FontWeight.SEMI_BOLD) gui_label(title_rect, entry.name, title_size, AetherListColors.HEADER, FontWeight.SEMI_BOLD)
meta_parts: list[str] = [] meta_parts: list[str] = []
@@ -585,7 +594,7 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
meta_parts.append(tr("Popular")) meta_parts.append(tr("Popular"))
if meta_parts: if meta_parts:
meta_rect = rl.Rectangle(rect.x, start_y + title_size + inter_gap, rect.width, meta_size) meta_rect = rl.Rectangle(rect.x, start_y + title_size * FONT_SCALE + inter_gap, rect.width, meta_size * FONT_SCALE)
has_warning = entry.partial or entry.requires_external_gpu has_warning = entry.partial or entry.requires_external_gpu
text_color = AetherListColors.WARNING if has_warning else AetherListColors.SUBTEXT text_color = AetherListColors.WARNING if has_warning else AetherListColors.SUBTEXT
gui_label(meta_rect, " • ".join(meta_parts), meta_size, text_color, FontWeight.NORMAL if not has_warning else FontWeight.MEDIUM) gui_label(meta_rect, " • ".join(meta_parts), meta_size, text_color, FontWeight.NORMAL if not has_warning else FontWeight.MEDIUM)
@@ -632,33 +641,31 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
) )
else: else:
btn_h = 48 btn_h = AETHER_LIST_METRICS.header_button_height
gap = 8 gap = SPACING.lg
total_h = btn_h * 2 + gap start_y = rect.y + (rect.height - btn_h) / 2
start_y = rect.y + (rect.height - total_h) / 2 btn_w = (rect.width - SPACING.md * 2 - gap) / 2
btn_w = rect.width - 28
delete_rect = rl.Rectangle(rect.x + 14, start_y, btn_w, btn_h) delete_rect = rl.Rectangle(rect.x + SPACING.md, start_y, btn_w, btn_h)
fav_rect = rl.Rectangle(rect.x + 14, start_y + btn_h + gap, btn_w, btn_h) fav_rect = rl.Rectangle(delete_rect.x + btn_w + gap, start_y, btn_w, btn_h)
self._interactive_rects[f"menu:{entry.key}:delete"] = delete_rect self._interactive_rects[f"menu:{entry.key}:delete"] = rl.Rectangle(delete_rect.x, rect.y, btn_w, rect.height)
self._interactive_rects[f"menu:{entry.key}:favorite"] = fav_rect self._interactive_rects[f"menu:{entry.key}:favorite"] = rl.Rectangle(fav_rect.x, rect.y, btn_w, rect.height)
draw_action_pill( draw_action_pill(
delete_rect, delete_rect,
tr("Delete"), tr("Delete"),
AetherListColors.DANGER_SOFT, AetherListColors.DANGER_SOFT,
rl.Color(AetherListColors.DANGER.r, AetherListColors.DANGER.g, AetherListColors.DANGER.b, min(AetherListColors.DANGER.a, 70)), rl.Color(AetherListColors.DANGER.r, AetherListColors.DANGER.g, AetherListColors.DANGER.b, min(AetherListColors.DANGER.a, 70)),
AetherListColors.DANGER, AetherListColors.HEADER,
font_size=24, font_size=32,
) )
is_fav = entry.user_favorite is_fav = entry.user_favorite
fav_fill = rl.Color(210, 100, 130, 44) if is_fav else rl.Color(PANEL_STYLE.accent.r, PANEL_STYLE.accent.g, PANEL_STYLE.accent.b, 26) fav_fill = rl.Color(210, 100, 130, 44) if is_fav else rl.Color(PANEL_STYLE.accent.r, PANEL_STYLE.accent.g, PANEL_STYLE.accent.b, 26)
fav_border = rl.Color((210 if is_fav else PANEL_STYLE.accent.r), (100 if is_fav else PANEL_STYLE.accent.g), (130 if is_fav else PANEL_STYLE.accent.b), min((255 if is_fav else PANEL_STYLE.accent.a), 70)) fav_border = rl.Color((210 if is_fav else PANEL_STYLE.accent.r), (100 if is_fav else PANEL_STYLE.accent.g), (130 if is_fav else PANEL_STYLE.accent.b), min((255 if is_fav else PANEL_STYLE.accent.a), 70))
fav_text_color = rl.Color(210, 100, 130, 255) if is_fav else PANEL_STYLE.accent
fav_label = tr("Unfavorite") if is_fav else tr("Favorite") fav_label = tr("Unfavorite") if is_fav else tr("Favorite")
draw_action_pill(fav_rect, fav_label, fav_fill, fav_border, fav_text_color, font_size=24) draw_action_pill(fav_rect, fav_label, fav_fill, fav_border, AetherListColors.HEADER, font_size=32)
def _draw_current_action(self, rect: rl.Rectangle): def _draw_current_action(self, rect: rl.Rectangle):
chip_h = 52 chip_h = 52
@@ -204,8 +204,7 @@ class StarPilotLateralLayout(_SettingsPage):
), ),
SettingRow( SettingRow(
"LaneChangeCloseGap", "toggle", tr_noop("Close Gap On Lane Change"), "LaneChangeCloseGap", "toggle", tr_noop("Close Gap On Lane Change"),
subtitle=tr_noop("Allows for a temporary shorter follow distance behind lead so that openpilot merges smoothly " + subtitle=tr_noop("Temporarily shorten the following gap and allow acceleration while changing lanes."),
"out of current lane, it will allow car to accelerate as it changes lanes."),
get_state=lambda: p.get_bool("LaneChangeCloseGap"), get_state=lambda: p.get_bool("LaneChangeCloseGap"),
set_state=lambda s: p.put_bool("LaneChangeCloseGap", s), set_state=lambda s: p.put_bool("LaneChangeCloseGap", s),
visible=lc_on, visible=lc_on,
@@ -628,7 +628,7 @@ class StarPilotLongitudinalLayout(_SettingsPage):
set_state=lambda s: self._params.put_bool("SLCMapboxFiller", s), set_state=lambda s: self._params.put_bool("SLCMapboxFiller", s),
visible=self._mapbox_available), visible=self._mapbox_available),
SettingRow("ShowSLCOffset", "toggle", tr_noop("Show SLC Offset"), SettingRow("ShowSLCOffset", "toggle", tr_noop("Show SLC Offset"),
subtitle=tr_noop("Compact display only; the unified card always shows nonzero offsets."), subtitle="",
get_state=lambda: self._params.get_bool("ShowSLCOffset"), get_state=lambda: self._params.get_bool("ShowSLCOffset"),
set_state=lambda s: self._params.put_bool("ShowSLCOffset", s)), set_state=lambda s: self._params.put_bool("ShowSLCOffset", s)),
SettingRow("SpeedLimitSources", "toggle", tr_noop("Show Sources"), SettingRow("SpeedLimitSources", "toggle", tr_noop("Show Sources"),
@@ -260,8 +260,6 @@ class MapsManagerView(PanelManagerView):
action_text_size=26, action_text_size=26,
action_pill_height=56, action_pill_height=56,
action_pill_width=154, action_pill_width=154,
title_size=32,
subtitle_size=22,
row_separator=PANEL_STYLE.divider_color, row_separator=PANEL_STYLE.divider_color,
current_bg=PANEL_STYLE.current_fill, current_bg=PANEL_STYLE.current_fill,
current_border=PANEL_STYLE.current_border, current_border=PANEL_STYLE.current_border,
@@ -307,8 +305,6 @@ class MapsManagerView(PanelManagerView):
action_text_size=26, action_text_size=26,
action_pill_height=56, action_pill_height=56,
action_pill_width=154 if selected else 128, action_pill_width=154 if selected else 128,
title_size=32,
subtitle_size=22,
row_separator=PANEL_STYLE.divider_color, row_separator=PANEL_STYLE.divider_color,
current_bg=PANEL_STYLE.current_fill, current_bg=PANEL_STYLE.current_fill,
current_border=PANEL_STYLE.current_border, current_border=PANEL_STYLE.current_border,

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