mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-03 12:53:55 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2efe6017bf | |||
| 3293a34219 | |||
| d2cd3fd0ed | |||
| 4ac874f6b4 | |||
| 1aefe9dc82 | |||
| 0857acfc50 | |||
| 5c3ddd58e1 | |||
| 7848a0090a | |||
| 86599a27bc | |||
| d9f58acbb7 | |||
| 42d4a5b207 | |||
| b22e2d678a | |||
| 26433e078e | |||
| bf1b916d50 |
@@ -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.
@@ -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
@@ -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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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>|||
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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.]
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
|||||||
DEV-96ef704d-DEBUG
|
DEV-d2cd3fd0-DEBUG
|
||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user