mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-02 20:33:44 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -3305,8 +3309,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 +3321,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 +3333,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 +3362,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
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
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)
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import math
|
import math
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
from openpilot.common.constants import CV
|
from openpilot.common.constants import CV
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||||
|
from openpilot.selfdrive.ui.onroad.hud_renderer import COLORS
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.selfdrive.ui.onroad.starpilot.widget_style import (
|
from openpilot.selfdrive.ui.onroad.starpilot.widget_style import (
|
||||||
CONTROL_BORDER, CONTROL_ROUNDNESS, CONTROL_SEGMENTS,
|
CONTROL_BG, CONTROL_BORDER, CONTROL_BORDER_WIDTH, CONTROL_ROUNDNESS, CONTROL_SEGMENTS, SLC_HEIGHT,
|
||||||
|
draw_control_card, roundness_for,
|
||||||
)
|
)
|
||||||
from openpilot.selfdrive.ui.onroad.starpilot.source_bubble_layout import (
|
from openpilot.selfdrive.ui.onroad.starpilot.source_bubble_layout import (
|
||||||
enabled_source_titles, fit_source_label, source_abbreviated_value_text,
|
enabled_source_titles, fit_source_label, source_abbreviated_value_text,
|
||||||
@@ -20,6 +23,14 @@ _WHITE = rl.Color(255, 255, 255, 255)
|
|||||||
|
|
||||||
# ── Constants ─────────────────────────────────────────────────────────
|
# ── Constants ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# EU Vienna sign
|
||||||
|
EU_SIGN_SIZE = 176
|
||||||
|
EU_SIGN_WIDTH = 176
|
||||||
|
RED_RING_WIDTH = 20
|
||||||
|
|
||||||
|
# Pending sign blink cadence — 1s period, 50% duty cycle.
|
||||||
|
PENDING_BLINK_MS = 500
|
||||||
|
|
||||||
# Source display metadata: source name, main label, value key, bubble label, icon.
|
# Source display metadata: source name, main label, value key, bubble label, icon.
|
||||||
SOURCE_DEFS = [
|
SOURCE_DEFS = [
|
||||||
("Dashboard", "Dash", "dashboard_sl", "Dashboard", "dashboard"),
|
("Dashboard", "Dash", "dashboard_sl", "Dashboard", "dashboard"),
|
||||||
@@ -28,12 +39,16 @@ SOURCE_DEFS = [
|
|||||||
("Mapbox", "MBOX", "mapbox_sl", "Mapbox", "map"),
|
("Mapbox", "MBOX", "mapbox_sl", "Mapbox", "map"),
|
||||||
("Upcoming", "NEXT", "next_sl", "Next", "next"),
|
("Upcoming", "NEXT", "next_sl", "Next", "next"),
|
||||||
]
|
]
|
||||||
_SOURCE_ICON_KEYS = {source: icon for source, _, _, _, icon in SOURCE_DEFS}
|
|
||||||
|
|
||||||
|
# Fonts
|
||||||
def source_icon_key(source: str) -> str | None:
|
FONT_LABEL = 30
|
||||||
"""Use the same source glyph as the detailed source diagnostics."""
|
FONT_SOURCE = 40 # Set Speed MAX label size.
|
||||||
return _SOURCE_ICON_KEYS.get(source)
|
FONT_SPEED = 90 # Set Speed value size.
|
||||||
|
FONT_OFFSET = 29 # Compact offset text.
|
||||||
|
OFFSET_CHIP_SEGMENTS = 8 # Capsule curve segments.
|
||||||
|
FONT_EU_LARGE = 70
|
||||||
|
FONT_EU_SMALL = 60
|
||||||
|
FONT_EU_OFFSET = 40
|
||||||
|
|
||||||
# Vision speed-limit pulse — one-shot purple highlight when the active source
|
# Vision speed-limit pulse — one-shot purple highlight when the active source
|
||||||
# is "Vision" and the resolved value just changed.
|
# is "Vision" and the resolved value just changed.
|
||||||
@@ -75,21 +90,8 @@ def _speed_limit_pulse_color(base: rl.Color, alpha: int) -> rl.Color:
|
|||||||
|
|
||||||
# ── State ─────────────────────────────────────────────────────────────
|
# ── State ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _is_slc_enabled() -> bool:
|
|
||||||
toggles = getattr(ui_state, "starpilot_toggles", {})
|
|
||||||
if "speed_limit_controller" in toggles:
|
|
||||||
return bool(toggles["speed_limit_controller"])
|
|
||||||
return ui_state.ui_params.get_bool("SpeedLimitController")
|
|
||||||
|
|
||||||
|
|
||||||
def _get_slc_state():
|
def _get_slc_state():
|
||||||
"""Extract SLC state from SubMaster. Returns dict or None if stale/hidden."""
|
"""Extract SLC state from SubMaster. Returns dict or None if stale/hidden."""
|
||||||
slc_enabled = _is_slc_enabled()
|
|
||||||
params = ui_state.ui_params
|
|
||||||
if not (slc_enabled or params.get_bool("ShowSpeedLimits")):
|
|
||||||
_pulse.clear()
|
|
||||||
return None
|
|
||||||
|
|
||||||
sm = ui_state.sm
|
sm = ui_state.sm
|
||||||
if sm.recv_frame["starpilotPlan"] < ui_state.started_frame:
|
if sm.recv_frame["starpilotPlan"] < ui_state.started_frame:
|
||||||
_pulse.clear()
|
_pulse.clear()
|
||||||
@@ -97,11 +99,18 @@ def _get_slc_state():
|
|||||||
|
|
||||||
plan = sm["starpilotPlan"]
|
plan = sm["starpilotPlan"]
|
||||||
speed_limit_changed = plan.speedLimitChanged
|
speed_limit_changed = plan.speedLimitChanged
|
||||||
presented_source = getattr(plan, 'slcPresentedSpeedLimitSource', '')
|
|
||||||
|
|
||||||
|
params = ui_state.ui_params
|
||||||
|
show_slc = params.get_bool("ShowSpeedLimits")
|
||||||
unconfirmed_valid = plan.unconfirmedSlcSpeedLimit > 1
|
unconfirmed_valid = plan.unconfirmedSlcSpeedLimit > 1
|
||||||
|
|
||||||
|
if not show_slc:
|
||||||
|
_pulse.clear()
|
||||||
|
return None
|
||||||
|
|
||||||
speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||||
|
show_offset = params.get_bool("ShowSLCOffset")
|
||||||
|
|
||||||
dashboard_sl = sm["starpilotCarState"].dashboardSpeedLimit if sm.valid.get("starpilotCarState", False) else 0.0
|
dashboard_sl = sm["starpilotCarState"].dashboardSpeedLimit if sm.valid.get("starpilotCarState", False) else 0.0
|
||||||
vision_enabled = params.get_bool("VisionSpeedLimitDetection")
|
vision_enabled = params.get_bool("VisionSpeedLimitDetection")
|
||||||
vision_sl = ui_state.params_memory.get_float("VisionSpeedLimit") if vision_enabled else 0.0
|
vision_sl = ui_state.params_memory.get_float("VisionSpeedLimit") if vision_enabled else 0.0
|
||||||
@@ -111,24 +120,41 @@ def _get_slc_state():
|
|||||||
params.get("MapboxSecretKey", encoding="utf-8")
|
params.get("MapboxSecretKey", encoding="utf-8")
|
||||||
)
|
)
|
||||||
|
|
||||||
# The pulse uses the accepted raw limit, so unit changes cannot retrigger it.
|
slc_overridden_speed = plan.slcOverriddenSpeed
|
||||||
_tick_pulse(plan.slcSpeedLimitSource, plan.slcSpeedLimit)
|
# Keep the source limit visible when overridden.
|
||||||
|
speed_limit = plan.slcSpeedLimit
|
||||||
|
|
||||||
|
# Resolved limit in m/s (pre-conversion, pre-offset) — feeds the vision pulse
|
||||||
|
# change detector so the comparison is unit-stable across km/h ↔ mph flips.
|
||||||
|
resolved_ms = speed_limit
|
||||||
|
|
||||||
|
# Add the per-limit offset to the displayed value only when NOT overridden
|
||||||
|
# AND ShowSLCOffset is off (when the offset toggle is on, it's rendered as
|
||||||
|
# a separate field below the speed number instead).
|
||||||
|
if slc_overridden_speed == 0 and not show_offset:
|
||||||
|
speed_limit += plan.slcSpeedLimitOffset
|
||||||
|
speed_limit *= speed_conversion
|
||||||
|
|
||||||
|
speed_limit_offset = plan.slcSpeedLimitOffset * speed_conversion
|
||||||
|
offset_str = f"{'+' if speed_limit_offset > 0 else '-'}{abs(int(round(speed_limit_offset)))}" if speed_limit_offset != 0 else "\u2013"
|
||||||
|
|
||||||
|
# Update the vision-source pulse once per frame, after resolved_ms is known
|
||||||
|
# and before any sign colors are computed downstream.
|
||||||
|
_tick_pulse(plan.slcSpeedLimitSource, resolved_ms)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'accepted_speed_limit_ms': plan.slcSpeedLimit,
|
'speed_limit': speed_limit,
|
||||||
# Match the control target's non-negative base before cluster compensation.
|
'speed_limit_str': "\u2013" if speed_limit <= 1 else str(int(round(speed_limit))),
|
||||||
'effective_target_ms': max(0.0, plan.slcSpeedLimit + plan.slcSpeedLimitOffset),
|
'slc_overridden_speed': slc_overridden_speed,
|
||||||
'offset_ms': plan.slcSpeedLimitOffset,
|
|
||||||
'slc_overridden_speed': plan.slcOverriddenSpeed,
|
|
||||||
'speed_limit_source': plan.slcSpeedLimitSource,
|
'speed_limit_source': plan.slcSpeedLimitSource,
|
||||||
# Older publishers/replays decode the new Text field as "", rather than omitting the attribute.
|
|
||||||
'presented_source': presented_source or plan.slcSpeedLimitSource,
|
|
||||||
'slc_enabled': slc_enabled,
|
|
||||||
# Both UI fields were added together; older plans have no published limiting state.
|
|
||||||
'slc_is_limiting_max_set': bool(getattr(plan, 'slcIsLimitingMaxSet', False)) if presented_source else None,
|
|
||||||
'unconfirmed_speed_limit': max(0.0, plan.unconfirmedSlcSpeedLimit * speed_conversion),
|
'unconfirmed_speed_limit': max(0.0, plan.unconfirmedSlcSpeedLimit * speed_conversion),
|
||||||
'unconfirmed_valid': unconfirmed_valid,
|
'unconfirmed_valid': unconfirmed_valid,
|
||||||
'speed_limit_changed': speed_limit_changed,
|
'speed_limit_changed': speed_limit_changed,
|
||||||
|
'show_offset': show_offset,
|
||||||
|
'use_vienna': params.get_bool("UseVienna"),
|
||||||
|
'offset_str': offset_str,
|
||||||
'speed_conversion': speed_conversion,
|
'speed_conversion': speed_conversion,
|
||||||
|
'speed_unit': " km/h" if ui_state.is_metric else " mph",
|
||||||
'slc_abbreviated_sources': params.get_bool("SLCAbbreviatedSources"),
|
'slc_abbreviated_sources': params.get_bool("SLCAbbreviatedSources"),
|
||||||
'slc_active_sources_only': params.get_bool("SLCActiveSourcesOnly"),
|
'slc_active_sources_only': params.get_bool("SLCActiveSourcesOnly"),
|
||||||
'slc_enabled_sources': enabled_source_titles(
|
'slc_enabled_sources': enabled_source_titles(
|
||||||
@@ -165,6 +191,203 @@ def _get_semi_bold():
|
|||||||
return _font_semi_bold
|
return _font_semi_bold
|
||||||
|
|
||||||
|
|
||||||
|
_ACTIVE_SOURCE_LABELS = {title: abbrev.upper() for title, abbrev, *_ in SOURCE_DEFS}
|
||||||
|
|
||||||
|
|
||||||
|
def _active_source_label(state: dict) -> str:
|
||||||
|
source = state.get("speed_limit_source")
|
||||||
|
if not source or source == "None":
|
||||||
|
return tr("LIMIT")
|
||||||
|
return _ACTIVE_SOURCE_LABELS.get(source, source.upper())
|
||||||
|
|
||||||
|
|
||||||
|
def _source_label_color(alpha: int, is_overridden: bool = False) -> rl.Color:
|
||||||
|
"""Match Set Speed's MAX label color."""
|
||||||
|
if is_overridden or ui_state.status in (UIStatus.DISENGAGED, UIStatus.OVERRIDE):
|
||||||
|
base = COLORS.DISENGAGED
|
||||||
|
elif ui_state.status == UIStatus.ENGAGED:
|
||||||
|
base = COLORS.ENGAGED
|
||||||
|
else:
|
||||||
|
base = COLORS.GREY
|
||||||
|
return _speed_limit_pulse_color(base, alpha)
|
||||||
|
|
||||||
|
|
||||||
|
# ── US MUTCD Sign ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _draw_offset_chip(rect: rl.Rectangle, offset_str: str, color: rl.Color) -> None:
|
||||||
|
"""Draw the optional SLC offset as a compact accent chip."""
|
||||||
|
font = _get_semi_bold()
|
||||||
|
text_size = measure_text_cached(font, offset_str, FONT_OFFSET)
|
||||||
|
chip_w = max(64.0, text_size.x + 24.0)
|
||||||
|
chip_h = 36.0
|
||||||
|
chip_rect = rl.Rectangle(
|
||||||
|
rect.x + (rect.width - chip_w) / 2,
|
||||||
|
rect.y + rect.height - chip_h - 10,
|
||||||
|
chip_w,
|
||||||
|
chip_h,
|
||||||
|
)
|
||||||
|
chip_fill = rl.Color(0, 0, 0, min(120, color.a))
|
||||||
|
roundness = roundness_for(chip_rect, 18)
|
||||||
|
rl.draw_rectangle_rounded(chip_rect, roundness, OFFSET_CHIP_SEGMENTS, chip_fill)
|
||||||
|
rl.draw_rectangle_rounded_lines_ex(chip_rect, roundness, OFFSET_CHIP_SEGMENTS, 2, color)
|
||||||
|
rl.draw_text_ex(
|
||||||
|
font,
|
||||||
|
offset_str,
|
||||||
|
rl.Vector2(chip_rect.x + (chip_w - text_size.x) / 2, chip_rect.y + (chip_h - text_size.y) / 2),
|
||||||
|
FONT_OFFSET,
|
||||||
|
0,
|
||||||
|
color,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_us_sign(x: float, y: float, sign_width: float, sign_height: float,
|
||||||
|
speed_text: str, offset_str: str,
|
||||||
|
source_label: str, alpha: int, show_offset: bool, *,
|
||||||
|
pending: bool = False, is_overridden: bool = False):
|
||||||
|
"""Draw the NA control card at (x, y).
|
||||||
|
|
||||||
|
The card keeps the SLC's label/value hierarchy while sharing the exact
|
||||||
|
visible frame geometry with Set Speed. Border and text colors continue to
|
||||||
|
use the existing Vision pulse and pending blink behavior.
|
||||||
|
"""
|
||||||
|
# Pending: blink white/red. Active: shared blue-grey.
|
||||||
|
if pending:
|
||||||
|
blink_on = int(rl.get_time() * 1000) % 1000 < PENDING_BLINK_MS
|
||||||
|
base_border = rl.Color(255, 255, 255, alpha) if blink_on else rl.Color(201, 34, 49, alpha)
|
||||||
|
else:
|
||||||
|
base_border = rl.Color(CONTROL_BORDER.r, CONTROL_BORDER.g, CONTROL_BORDER.b,
|
||||||
|
min(alpha, CONTROL_BORDER.a))
|
||||||
|
|
||||||
|
# Compose the blink base with the active vision pulse (no-op outside window).
|
||||||
|
border_color = _speed_limit_pulse_color(base_border, base_border.a)
|
||||||
|
# White value text reads on the translucent road background.
|
||||||
|
text_color = _speed_limit_pulse_color(rl.Color(255, 255, 255, 255), alpha)
|
||||||
|
|
||||||
|
card_rect = rl.Rectangle(x, y, sign_width, sign_height)
|
||||||
|
card_fill = rl.Color(CONTROL_BG.r, CONTROL_BG.g, CONTROL_BG.b, min(CONTROL_BG.a, alpha))
|
||||||
|
draw_control_card(card_rect, fill=card_fill, border=border_color,
|
||||||
|
border_width=CONTROL_BORDER_WIDTH)
|
||||||
|
|
||||||
|
font_bold = _get_bold()
|
||||||
|
font_semi = _get_semi_bold()
|
||||||
|
cx = x + sign_width / 2
|
||||||
|
|
||||||
|
# Pending layout: "PENDING" + "LIMIT" + speed (no offset shown when pending).
|
||||||
|
if pending:
|
||||||
|
pending_size = measure_text_cached(font_semi, tr("PENDING"), FONT_LABEL - 2)
|
||||||
|
rl.draw_text_ex(font_semi, tr("PENDING"), rl.Vector2(cx - pending_size.x / 2, y + 20), FONT_LABEL - 2, 0, text_color)
|
||||||
|
limit_size = measure_text_cached(font_semi, tr("LIMIT"), FONT_LABEL)
|
||||||
|
rl.draw_text_ex(font_semi, tr("LIMIT"), rl.Vector2(cx - limit_size.x / 2, y + 48), FONT_LABEL, 0, text_color)
|
||||||
|
speed_size = measure_text_cached(font_bold, speed_text, FONT_SPEED - 6)
|
||||||
|
rl.draw_text_ex(font_bold, speed_text, rl.Vector2(cx - speed_size.x / 2, y + 85), FONT_SPEED - 6, 0, text_color)
|
||||||
|
elif show_offset:
|
||||||
|
# Offset ON: source at the top, speed below it, and the offset in a chip.
|
||||||
|
source_size = measure_text_cached(font_semi, source_label, FONT_SOURCE)
|
||||||
|
source_color = _source_label_color(alpha, is_overridden=is_overridden)
|
||||||
|
rl.draw_text_ex(font_semi, source_label, rl.Vector2(cx - source_size.x / 2, y + 8), FONT_SOURCE, 0, source_color)
|
||||||
|
|
||||||
|
speed_size = measure_text_cached(font_bold, speed_text, FONT_SPEED)
|
||||||
|
rl.draw_text_ex(font_bold, speed_text, rl.Vector2(cx - speed_size.x / 2, y + 44), FONT_SPEED, 0, text_color)
|
||||||
|
_draw_offset_chip(card_rect, offset_str, text_color)
|
||||||
|
else:
|
||||||
|
# Offset OFF: match Set Speed typography.
|
||||||
|
source_size = measure_text_cached(font_semi, source_label, FONT_SOURCE)
|
||||||
|
source_color = _source_label_color(alpha, is_overridden=is_overridden)
|
||||||
|
rl.draw_text_ex(font_semi, source_label, rl.Vector2(cx - source_size.x / 2, y + 27), FONT_SOURCE, 0, source_color)
|
||||||
|
|
||||||
|
speed_size = measure_text_cached(font_bold, speed_text, FONT_SPEED)
|
||||||
|
rl.draw_text_ex(font_bold, speed_text, rl.Vector2(cx - speed_size.x / 2, y + 77), FONT_SPEED, 0, text_color)
|
||||||
|
|
||||||
|
|
||||||
|
# ── EU Vienna Sign ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _draw_eu_sign(x: float, y: float, speed_text: str, offset_str: str,
|
||||||
|
source_label: str, text_alpha: int, show_offset: bool, *, pending: bool = False):
|
||||||
|
"""Draw EU-style (Vienna) speed limit sign at (x, y).
|
||||||
|
|
||||||
|
White disk with a pulsable red ring and pulsable black text. The pre-existing
|
||||||
|
pending-text blink (black <-> red) composes with the vision pulse: outside the
|
||||||
|
pulse window the blink is unchanged, inside it both colors are eased toward
|
||||||
|
VISION_SPEED_LIMIT_PULSE_COLOR.
|
||||||
|
"""
|
||||||
|
center_x = x + EU_SIGN_SIZE / 2
|
||||||
|
center_y = y + EU_SIGN_SIZE / 2
|
||||||
|
radius = EU_SIGN_SIZE / 2
|
||||||
|
|
||||||
|
# White disk fill.
|
||||||
|
rl.draw_circle(int(center_x), int(center_y), radius, rl.Color(255, 255, 255, text_alpha))
|
||||||
|
# Red ring; eased toward VISION_SPEED_LIMIT_PULSE_COLOR when a Vision-sourced
|
||||||
|
# limit just changed.
|
||||||
|
ring_color = _speed_limit_pulse_color(rl.Color(201, 34, 49, 255), text_alpha)
|
||||||
|
rl.draw_ring(rl.Vector2(center_x, center_y), radius - RED_RING_WIDTH, radius,
|
||||||
|
0, 360, 64, ring_color)
|
||||||
|
|
||||||
|
font_bold = _get_bold()
|
||||||
|
|
||||||
|
eu_font = FONT_EU_LARGE if len(speed_text) <= 2 else FONT_EU_SMALL
|
||||||
|
|
||||||
|
# EU pending: text blinks black/red, composed with the vision pulse.
|
||||||
|
if pending:
|
||||||
|
blink_on = int(rl.get_time() * 1000) % 1000 < PENDING_BLINK_MS
|
||||||
|
base_text = rl.Color(0, 0, 0, 255) if blink_on else rl.Color(201, 34, 49, 255)
|
||||||
|
else:
|
||||||
|
base_text = rl.Color(0, 0, 0, 255)
|
||||||
|
text_color = _speed_limit_pulse_color(base_text, text_alpha)
|
||||||
|
|
||||||
|
# Pending: text centered (no offset display)
|
||||||
|
if pending:
|
||||||
|
speed_size = measure_text_cached(font_bold, speed_text, eu_font)
|
||||||
|
speed_pos = rl.Vector2(center_x - speed_size.x / 2, center_y - speed_size.y / 2)
|
||||||
|
rl.draw_text_ex(font_bold, speed_text, speed_pos, eu_font, 0, text_color)
|
||||||
|
elif not show_offset:
|
||||||
|
font_semi = _get_semi_bold()
|
||||||
|
source_size = measure_text_cached(font_semi, source_label, FONT_LABEL - 4)
|
||||||
|
source_pos = rl.Vector2(center_x - source_size.x / 2, y + 16)
|
||||||
|
rl.draw_text_ex(font_semi, source_label, source_pos, FONT_LABEL - 4, 0, text_color)
|
||||||
|
|
||||||
|
speed_size = measure_text_cached(font_bold, speed_text, eu_font)
|
||||||
|
speed_pos = rl.Vector2(center_x - speed_size.x / 2, center_y - speed_size.y / 2)
|
||||||
|
rl.draw_text_ex(font_bold, speed_text, speed_pos, eu_font, 0, text_color)
|
||||||
|
else:
|
||||||
|
# Offset ON: source at the top, speed below it, offset at the bottom.
|
||||||
|
font_semi = _get_semi_bold()
|
||||||
|
source_size = measure_text_cached(font_semi, source_label, FONT_LABEL - 4)
|
||||||
|
source_pos = rl.Vector2(center_x - source_size.x / 2, y + 16)
|
||||||
|
rl.draw_text_ex(font_semi, source_label, source_pos, FONT_LABEL - 4, 0, text_color)
|
||||||
|
|
||||||
|
speed_size = measure_text_cached(font_bold, speed_text, eu_font)
|
||||||
|
speed_pos = rl.Vector2(center_x - speed_size.x / 2, center_y - speed_size.y / 2 - 5)
|
||||||
|
rl.draw_text_ex(font_bold, speed_text, speed_pos, eu_font, 0, text_color)
|
||||||
|
|
||||||
|
offset_size = measure_text_cached(font_semi, offset_str, FONT_EU_OFFSET)
|
||||||
|
offset_pos = rl.Vector2(center_x - offset_size.x / 2, y + 122)
|
||||||
|
rl.draw_text_ex(font_semi, offset_str, offset_pos, FONT_EU_OFFSET, 0, text_color)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Dispatcher (pending and active sign share the same rect) ─────────
|
||||||
|
|
||||||
|
def _draw_sign(state: dict, rect: rl.Rectangle, *, pending: bool = False):
|
||||||
|
"""Draw either the pending or active sign in the given rect."""
|
||||||
|
if pending:
|
||||||
|
# Pending shows the unconfirmed value, full opacity
|
||||||
|
speed_text = ("\u2013" if state['unconfirmed_speed_limit'] <= 1
|
||||||
|
else str(int(round(state['unconfirmed_speed_limit']))))
|
||||||
|
else:
|
||||||
|
speed_text = state['speed_limit_str']
|
||||||
|
|
||||||
|
text_alpha = 255
|
||||||
|
is_overridden = not pending and state['slc_overridden_speed'] != 0
|
||||||
|
source_label = _active_source_label(state)
|
||||||
|
|
||||||
|
if state['use_vienna']:
|
||||||
|
_draw_eu_sign(rect.x, rect.y, speed_text, state['offset_str'], source_label, text_alpha,
|
||||||
|
state['show_offset'], pending=pending)
|
||||||
|
else:
|
||||||
|
_draw_us_sign(rect.x, rect.y, rect.width, rect.height, speed_text, state['offset_str'],
|
||||||
|
source_label, text_alpha, state['show_offset'], pending=pending,
|
||||||
|
is_overridden=is_overridden)
|
||||||
|
|
||||||
|
|
||||||
# ── Sources Bubble (expandable overlay) ────────────────────────────────
|
# ── Sources Bubble (expandable overlay) ────────────────────────────────
|
||||||
|
|
||||||
# Fixed outer footprint; the content scale adapts to the visible row count.
|
# Fixed outer footprint; the content scale adapts to the visible row count.
|
||||||
@@ -194,7 +417,7 @@ _SOURCE_COMPACT_LABELS = {
|
|||||||
|
|
||||||
|
|
||||||
def _draw_source_icon(icon_key: str, x: float, y: float, size: float, color: rl.Color) -> None:
|
def _draw_source_icon(icon_key: str, x: float, y: float, size: float, color: rl.Color) -> None:
|
||||||
"""Draw the existing source glyph for both the header and diagnostics."""
|
"""Draw the small, intentionally simple source glyphs used by the panel."""
|
||||||
cx = x + size / 2
|
cx = x + size / 2
|
||||||
cy = y + size / 2
|
cy = y + size / 2
|
||||||
stroke = max(2.5, size / 12.0)
|
stroke = max(2.5, size / 12.0)
|
||||||
@@ -257,20 +480,11 @@ def _draw_source_icon(icon_key: str, x: float, y: float, size: float, color: rl.
|
|||||||
color,
|
color,
|
||||||
)
|
)
|
||||||
rl.draw_circle_v(pin_center, size * 0.09, _SOURCE_PANEL_BG)
|
rl.draw_circle_v(pin_center, size * 0.09, _SOURCE_PANEL_BG)
|
||||||
elif icon_key == "dashboard":
|
else: # Dashboard / fallback
|
||||||
# The Dashboard speed-limit source is a vehicle glyph, distinct from Max Set's gauge.
|
dashboard_scale = 1.22
|
||||||
body = rl.Rectangle(x + size * 0.10, y + size * 0.43, size * 0.80, size * 0.29)
|
|
||||||
rl.draw_rectangle_rounded_lines_ex(body, 0.30, 8, stroke, color)
|
|
||||||
rl.draw_line_ex(rl.Vector2(x + size * 0.25, body.y), rl.Vector2(x + size * 0.36, y + size * 0.27), stroke, color)
|
|
||||||
rl.draw_line_ex(rl.Vector2(x + size * 0.36, y + size * 0.27), rl.Vector2(x + size * 0.68, y + size * 0.27), stroke, color)
|
|
||||||
rl.draw_line_ex(rl.Vector2(x + size * 0.68, y + size * 0.27), rl.Vector2(x + size * 0.79, body.y), stroke, color)
|
|
||||||
for wheel_x in (x + size * 0.27, x + size * 0.73):
|
|
||||||
rl.draw_circle_v(rl.Vector2(wheel_x, y + size * 0.75), size * 0.07, color)
|
|
||||||
elif icon_key == "speedometer":
|
|
||||||
gauge_scale = 1.22
|
|
||||||
pivot = rl.Vector2(cx, cy + size * 0.17)
|
pivot = rl.Vector2(cx, cy + size * 0.17)
|
||||||
inner_radius = size * 0.27 * gauge_scale
|
inner_radius = size * 0.27 * dashboard_scale
|
||||||
outer_radius = size * 0.34 * gauge_scale
|
outer_radius = size * 0.34 * dashboard_scale
|
||||||
ring_segments = max(24, int(size * 0.25))
|
ring_segments = max(24, int(size * 0.25))
|
||||||
rl.draw_ring(pivot, inner_radius, outer_radius, 190, 350, ring_segments, color)
|
rl.draw_ring(pivot, inner_radius, outer_radius, 190, 350, ring_segments, color)
|
||||||
cap_radius = (outer_radius - inner_radius) / 2
|
cap_radius = (outer_radius - inner_radius) / 2
|
||||||
@@ -295,7 +509,7 @@ def _draw_source_icon(icon_key: str, x: float, y: float, size: float, color: rl.
|
|||||||
stroke,
|
stroke,
|
||||||
color,
|
color,
|
||||||
)
|
)
|
||||||
rl.draw_circle_v(pivot, max(2.0, size * 0.06 * gauge_scale), color)
|
rl.draw_circle_v(pivot, max(2.0, size * 0.06 * dashboard_scale), color)
|
||||||
|
|
||||||
|
|
||||||
def _draw_sources_bubble_empty_state(panel_rect: rl.Rectangle) -> None:
|
def _draw_sources_bubble_empty_state(panel_rect: rl.Rectangle) -> None:
|
||||||
@@ -309,7 +523,7 @@ def _draw_sources_bubble_empty_state(panel_rect: rl.Rectangle) -> None:
|
|||||||
total_h = sum(sz.y for sz in line_sizes) + line_gap * (len(lines) - 1)
|
total_h = sum(sz.y for sz in line_sizes) + line_gap * (len(lines) - 1)
|
||||||
curr_y = round(panel_rect.y + (panel_rect.height - total_h) / 2)
|
curr_y = round(panel_rect.y + (panel_rect.height - total_h) / 2)
|
||||||
|
|
||||||
for line, sz in zip(lines, line_sizes, strict=True):
|
for line, sz in zip(lines, line_sizes):
|
||||||
pos_x = round(panel_rect.x + (panel_rect.width - sz.x) / 2)
|
pos_x = round(panel_rect.x + (panel_rect.width - sz.x) / 2)
|
||||||
rl.draw_text_ex(font, line, rl.Vector2(pos_x, curr_y), font_size, 0, _WHITE)
|
rl.draw_text_ex(font, line, rl.Vector2(pos_x, curr_y), font_size, 0, _WHITE)
|
||||||
curr_y += round(sz.y + line_gap)
|
curr_y += round(sz.y + line_gap)
|
||||||
@@ -394,7 +608,7 @@ def _draw_sources_bubble(state: dict, sign_rect: rl.Rectangle):
|
|||||||
f"{tr(compact_label)}-{source_abbreviated_value_text(value)}",
|
f"{tr(compact_label)}-{source_abbreviated_value_text(value)}",
|
||||||
"",
|
"",
|
||||||
content_right - label_left,
|
content_right - label_left,
|
||||||
lambda text, font=text_font: measure_text_cached(font, text, font_size).x,
|
lambda text: measure_text_cached(text_font, text, font_size).x,
|
||||||
)
|
)
|
||||||
label_size = measure_text_cached(text_font, label_text, font_size)
|
label_size = measure_text_cached(text_font, label_text, font_size)
|
||||||
text_y = round(row_y + (row_h - label_size.y) / 2)
|
text_y = round(row_y + (row_h - label_size.y) / 2)
|
||||||
@@ -433,3 +647,24 @@ def _draw_sources_bubble(state: dict, sign_rect: rl.Rectangle):
|
|||||||
value_pos = rl.Vector2(round(content_right - value_size.x), text_y)
|
value_pos = rl.Vector2(round(content_right - value_size.x), text_y)
|
||||||
rl.draw_text_ex(font_semi, label_text, label_pos, font_size, 0, text_color)
|
rl.draw_text_ex(font_semi, label_text, label_pos, font_size, 0, text_color)
|
||||||
rl.draw_text_ex(font_bold, value_text, value_pos, font_size, 0, text_color)
|
rl.draw_text_ex(font_bold, value_text, value_pos, font_size, 0, text_color)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Public API ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def render_speed_limit_at(state: dict, rect: rl.Rectangle, expanded: bool = False) -> Optional[rl.Rectangle]:
|
||||||
|
"""Render the SLC sign and optional source bubble at a layout rect."""
|
||||||
|
flashing_pending = state['speed_limit_changed'] and state['unconfirmed_valid']
|
||||||
|
|
||||||
|
if flashing_pending:
|
||||||
|
_draw_sign(state, rect, pending=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
_draw_sign(state, rect, pending=False)
|
||||||
|
|
||||||
|
use_vienna = state['use_vienna']
|
||||||
|
visual_rect = rl.Rectangle(rect.x, rect.y, EU_SIGN_SIZE, EU_SIGN_SIZE) if use_vienna else rect
|
||||||
|
|
||||||
|
if expanded:
|
||||||
|
_draw_sources_bubble(state, visual_rect)
|
||||||
|
|
||||||
|
return visual_rect
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from openpilot.selfdrive.ui.onroad.starpilot.torque_bar import TorqueBar
|
|||||||
from openpilot.selfdrive.ui.onroad.starpilot.rivian_lateral_mode import rivian_lateral_mode
|
from openpilot.selfdrive.ui.onroad.starpilot.rivian_lateral_mode import rivian_lateral_mode
|
||||||
from openpilot.selfdrive.ui.onroad.starpilot.widget_layout_manager import WidgetLayoutManager
|
from openpilot.selfdrive.ui.onroad.starpilot.widget_layout_manager import WidgetLayoutManager
|
||||||
from openpilot.selfdrive.ui.onroad.starpilot.widgets import (
|
from openpilot.selfdrive.ui.onroad.starpilot.widgets import (
|
||||||
UnifiedSpeedWidget, PedalIconsWidget,
|
SetSpeedWidget, SpeedLimitWidget, PedalIconsWidget,
|
||||||
AetherGaugeWidget, PersonalityButtonWidget, DriverMonitorWidget,
|
AetherGaugeWidget, PersonalityButtonWidget, DriverMonitorWidget,
|
||||||
SteeringWheelWidget, StoppedTimerWidget, ModelSourceWidget
|
SteeringWheelWidget, StoppedTimerWidget, ModelSourceWidget
|
||||||
)
|
)
|
||||||
@@ -25,6 +25,7 @@ from openpilot.starpilot.common.favorite_slots import (
|
|||||||
build_favorite_slot_options,
|
build_favorite_slot_options,
|
||||||
filter_favorite_slot_options,
|
filter_favorite_slot_options,
|
||||||
favorite_key_is_valid,
|
favorite_key_is_valid,
|
||||||
|
is_bool_param,
|
||||||
)
|
)
|
||||||
|
|
||||||
from openpilot.system.ui.lib.application import MousePos, gui_app, FontWeight
|
from openpilot.system.ui.lib.application import MousePos, gui_app, FontWeight
|
||||||
@@ -63,7 +64,8 @@ class StarPilotOnroadView(AugmentedRoadView):
|
|||||||
self._hud_renderer.draw_exp_button = False
|
self._hud_renderer.draw_exp_button = False
|
||||||
|
|
||||||
# Initialize layout widgets
|
# Initialize layout widgets
|
||||||
self._unified_speed_widget = UnifiedSpeedWidget(self._hud_renderer)
|
self._set_speed_widget = SetSpeedWidget(self._hud_renderer)
|
||||||
|
self._speed_limit_widget = SpeedLimitWidget()
|
||||||
self._aethergauge_widget = AetherGaugeWidget(self._hud_renderer)
|
self._aethergauge_widget = AetherGaugeWidget(self._hud_renderer)
|
||||||
self._steering_wheel_widget = SteeringWheelWidget(self._hud_renderer._exp_button)
|
self._steering_wheel_widget = SteeringWheelWidget(self._hud_renderer._exp_button)
|
||||||
self._pedals_widget = PedalIconsWidget()
|
self._pedals_widget = PedalIconsWidget()
|
||||||
@@ -73,7 +75,8 @@ class StarPilotOnroadView(AugmentedRoadView):
|
|||||||
self._stopped_timer_widget = StoppedTimerWidget(self.is_in_reverse)
|
self._stopped_timer_widget = StoppedTimerWidget(self.is_in_reverse)
|
||||||
|
|
||||||
# Register to layout zones
|
# Register to layout zones
|
||||||
self.layout_manager.register_widget("left", self._unified_speed_widget)
|
self.layout_manager.register_widget("left", self._set_speed_widget)
|
||||||
|
self.layout_manager.register_widget("left", self._speed_limit_widget)
|
||||||
self.layout_manager.register_widget("left", self._aethergauge_widget)
|
self.layout_manager.register_widget("left", self._aethergauge_widget)
|
||||||
self.layout_manager.register_widget("right", self._steering_wheel_widget)
|
self.layout_manager.register_widget("right", self._steering_wheel_widget)
|
||||||
self.layout_manager.register_widget("right", self._pedals_widget)
|
self.layout_manager.register_widget("right", self._pedals_widget)
|
||||||
@@ -82,7 +85,8 @@ class StarPilotOnroadView(AugmentedRoadView):
|
|||||||
self.layout_manager.register_widget("bottom", self._driver_monitor_widget)
|
self.layout_manager.register_widget("bottom", self._driver_monitor_widget)
|
||||||
|
|
||||||
# Register as child widgets for click propagation
|
# Register as child widgets for click propagation
|
||||||
self._child(self._unified_speed_widget)
|
self._child(self._set_speed_widget)
|
||||||
|
self._child(self._speed_limit_widget)
|
||||||
self._child(self._aethergauge_widget)
|
self._child(self._aethergauge_widget)
|
||||||
self._child(self._steering_wheel_widget)
|
self._child(self._steering_wheel_widget)
|
||||||
self._child(self._pedals_widget)
|
self._child(self._pedals_widget)
|
||||||
@@ -129,7 +133,7 @@ class StarPilotOnroadView(AugmentedRoadView):
|
|||||||
if self._draw_hud_controls:
|
if self._draw_hud_controls:
|
||||||
dm = self.driver_state_renderer
|
dm = self.driver_state_renderer
|
||||||
self.layout_manager.update_layout(self._content_rect, is_rhd=dm.is_rhd if dm else False)
|
self.layout_manager.update_layout(self._content_rect, is_rhd=dm.is_rhd if dm else False)
|
||||||
self._render_speed_card()
|
self._render_slc()
|
||||||
self._render_overlays()
|
self._render_overlays()
|
||||||
self._render_road_name()
|
self._render_road_name()
|
||||||
|
|
||||||
@@ -163,11 +167,13 @@ class StarPilotOnroadView(AugmentedRoadView):
|
|||||||
render_background_effects(rect, border_width)
|
render_background_effects(rect, border_width)
|
||||||
render_overlay(border_rect, border_width)
|
render_overlay(border_rect, border_width)
|
||||||
|
|
||||||
def _render_speed_card(self):
|
def _render_slc(self):
|
||||||
if self._full_alert_showing():
|
if self._full_alert_showing():
|
||||||
return
|
return
|
||||||
if self._unified_speed_widget.is_visible:
|
if self._speed_limit_widget.is_visible:
|
||||||
self._unified_speed_widget.render(self._unified_speed_widget.rect)
|
self._speed_limit_widget.render(self._speed_limit_widget.rect)
|
||||||
|
if self._set_speed_widget.is_visible:
|
||||||
|
self._set_speed_widget.render(self._set_speed_widget.rect)
|
||||||
|
|
||||||
def _render_overlays(self):
|
def _render_overlays(self):
|
||||||
alert_showing, _ = self.alert_renderer.will_render()
|
alert_showing, _ = self.alert_renderer.will_render()
|
||||||
@@ -180,7 +186,7 @@ class StarPilotOnroadView(AugmentedRoadView):
|
|||||||
|
|
||||||
self._render_developer_metrics()
|
self._render_developer_metrics()
|
||||||
|
|
||||||
self.layout_manager.render_widgets(exclude={"unified_speed"})
|
self.layout_manager.render_widgets(exclude={"speed_limit", "set_speed"})
|
||||||
|
|
||||||
self._render_torque_bar()
|
self._render_torque_bar()
|
||||||
self._render_bottom_row_widgets()
|
self._render_bottom_row_widgets()
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
"""Displayed Max Set and posted-limit values for the Big UI speed card."""
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class UnifiedSpeedPresentation:
|
|
||||||
mode: str
|
|
||||||
max_speed_text: str
|
|
||||||
posted_speed_text: str
|
|
||||||
effective_speed_text: str
|
|
||||||
offset_text: str | None
|
|
||||||
unit_text: str
|
|
||||||
source: str
|
|
||||||
confirmation_pending: bool
|
|
||||||
active_side: str
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_unified_speed(show_max: bool, cruise_set: bool, max_speed: float,
|
|
||||||
slc_state: dict | None, slc_enabled: bool, is_metric: bool) -> UnifiedSpeedPresentation:
|
|
||||||
"""Compare the rounded values the driver sees; ignore override speed for layout."""
|
|
||||||
unit = "km/h" if is_metric else "mph"
|
|
||||||
max_text = str(round(max_speed)) if cruise_set else "–"
|
|
||||||
posted_text = effective_text = "–"
|
|
||||||
offset_text = None
|
|
||||||
source = "None"
|
|
||||||
pending = has_limit = slc_is_limiting = False
|
|
||||||
if slc_state is not None:
|
|
||||||
conversion = slc_state['speed_conversion']
|
|
||||||
accepted = slc_state['accepted_speed_limit_ms']
|
|
||||||
pending = bool(slc_state['speed_limit_changed'] and slc_state['unconfirmed_valid'])
|
|
||||||
source = slc_state['presented_source']
|
|
||||||
has_limit = (source not in ("", "None") and accepted > 1) or pending
|
|
||||||
if has_limit:
|
|
||||||
posted_text = str(round(slc_state['unconfirmed_speed_limit'])) if pending else str(round(accepted * conversion))
|
|
||||||
effective = slc_state['effective_target_ms']
|
|
||||||
slc_is_limiting = slc_state['slc_is_limiting_max_set']
|
|
||||||
if slc_is_limiting is None:
|
|
||||||
slc_is_limiting = cruise_set and accepted > 1 and 0 < effective * conversion < max_speed
|
|
||||||
effective_text = str(round(effective * conversion)) if effective > 0 else "–"
|
|
||||||
offset_display = round(slc_state['offset_ms'] * conversion)
|
|
||||||
offset_text = f"{offset_display:+d}" if offset_display else None
|
|
||||||
else:
|
|
||||||
source = "None"
|
|
||||||
|
|
||||||
# Max-only is valid only when SLC is disabled.
|
|
||||||
if pending:
|
|
||||||
mode = "split"
|
|
||||||
elif slc_enabled:
|
|
||||||
mode = "merged" if show_max and cruise_set and has_limit and max_text == effective_text else "split" if show_max else "limit_only"
|
|
||||||
elif has_limit:
|
|
||||||
mode = "split" if show_max else "limit_only"
|
|
||||||
else:
|
|
||||||
mode = "max_only"
|
|
||||||
|
|
||||||
active_side = "none" if slc_state is not None and slc_state['slc_overridden_speed'] else "shared" if mode == "merged" else (
|
|
||||||
"slc" if slc_enabled and cruise_set and slc_is_limiting else
|
|
||||||
"max" if (show_max or pending) and cruise_set else "none"
|
|
||||||
)
|
|
||||||
return UnifiedSpeedPresentation(mode, max_text, posted_text, effective_text, offset_text, unit, source, pending, active_side)
|
|
||||||
@@ -30,12 +30,12 @@ class WidgetLayoutManager:
|
|||||||
active_widgets = [w for w in self.zones["left"] if w.is_visible]
|
active_widgets = [w for w in self.zones["left"] if w.is_visible]
|
||||||
|
|
||||||
# Left zone stacks vertically from the top-left offset
|
# Left zone stacks vertically from the top-left offset
|
||||||
# Keep wide cards inside the content rect without moving compact widgets.
|
# X anchor is the shared left-control center (content x + 146).
|
||||||
|
center_x = self.content_rect.x + WIDGET_ANCHOR_OFFSET
|
||||||
current_y = self.content_rect.y + 45
|
current_y = self.content_rect.y + 45
|
||||||
|
|
||||||
for widget in active_widgets:
|
for widget in active_widgets:
|
||||||
w, h = widget.get_size()
|
w, h = widget.get_size()
|
||||||
center_x = self.content_rect.x + max(float(WIDGET_ANCHOR_OFFSET), w / 2 + 30)
|
|
||||||
widget.set_rect(rl.Rectangle(center_x - w / 2, current_y, w, h))
|
widget.set_rect(rl.Rectangle(center_x - w / 2, current_y, w, h))
|
||||||
current_y += h + self.spacing
|
current_y += h + self.spacing
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from openpilot.selfdrive.ui.onroad.starpilot.widgets.base import LayoutWidget
|
from openpilot.selfdrive.ui.onroad.starpilot.widgets.base import LayoutWidget
|
||||||
from openpilot.selfdrive.ui.onroad.starpilot.widgets.unified_speed import UnifiedSpeedWidget
|
from openpilot.selfdrive.ui.onroad.starpilot.widgets.set_speed import SetSpeedWidget
|
||||||
|
from openpilot.selfdrive.ui.onroad.starpilot.widgets.speed_limit import SpeedLimitWidget
|
||||||
from openpilot.selfdrive.ui.onroad.starpilot.widgets.pedal_icons import PedalIconsWidget
|
from openpilot.selfdrive.ui.onroad.starpilot.widgets.pedal_icons import PedalIconsWidget
|
||||||
from openpilot.selfdrive.ui.onroad.starpilot.widgets.aethergauge import AetherGaugeWidget
|
from openpilot.selfdrive.ui.onroad.starpilot.widgets.aethergauge import AetherGaugeWidget
|
||||||
from openpilot.selfdrive.ui.onroad.starpilot.widgets.personality_button import PersonalityButtonWidget
|
from openpilot.selfdrive.ui.onroad.starpilot.widgets.personality_button import PersonalityButtonWidget
|
||||||
@@ -10,7 +11,8 @@ from openpilot.selfdrive.ui.onroad.starpilot.widgets.model_source import ModelSo
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"LayoutWidget",
|
"LayoutWidget",
|
||||||
"UnifiedSpeedWidget",
|
"SetSpeedWidget",
|
||||||
|
"SpeedLimitWidget",
|
||||||
"PedalIconsWidget",
|
"PedalIconsWidget",
|
||||||
"AetherGaugeWidget",
|
"AetherGaugeWidget",
|
||||||
"PersonalityButtonWidget",
|
"PersonalityButtonWidget",
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import pyray as rl
|
||||||
|
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||||
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
|
from openpilot.selfdrive.ui.onroad.starpilot.widgets.base import LayoutWidget
|
||||||
|
from openpilot.selfdrive.ui.onroad.hud_renderer import (
|
||||||
|
UI_CONFIG, FONT_SIZES, COLORS, CRUISE_DISABLED_CHAR
|
||||||
|
)
|
||||||
|
from openpilot.selfdrive.ui.onroad.starpilot.widget_style import draw_control_card
|
||||||
|
|
||||||
|
class SetSpeedWidget(LayoutWidget):
|
||||||
|
def __init__(self, hud_renderer):
|
||||||
|
super().__init__("set_speed", priority=1)
|
||||||
|
self.hud_renderer = hud_renderer
|
||||||
|
self._font_semi_bold = gui_app.font(FontWeight.SEMI_BOLD)
|
||||||
|
self._font_bold = gui_app.font(FontWeight.BOLD)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_visible(self) -> bool:
|
||||||
|
return (
|
||||||
|
self.hud_renderer.is_cruise_available
|
||||||
|
and not ui_state.starpilot_toggles.get("hide_max_speed", False)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_size(self) -> tuple[float, float]:
|
||||||
|
set_speed_width = (
|
||||||
|
UI_CONFIG.set_speed_width_metric
|
||||||
|
if ui_state.is_metric
|
||||||
|
else UI_CONFIG.set_speed_width_imperial
|
||||||
|
)
|
||||||
|
return float(set_speed_width), float(UI_CONFIG.set_speed_height)
|
||||||
|
|
||||||
|
def _render(self, rect: rl.Rectangle) -> None:
|
||||||
|
draw_control_card(rect)
|
||||||
|
|
||||||
|
max_color = COLORS.GREY
|
||||||
|
set_speed_color = COLORS.DARK_GREY
|
||||||
|
if self.hud_renderer.is_cruise_set:
|
||||||
|
set_speed_color = COLORS.WHITE
|
||||||
|
if ui_state.status == UIStatus.ENGAGED:
|
||||||
|
max_color = COLORS.ENGAGED
|
||||||
|
elif ui_state.status == UIStatus.DISENGAGED:
|
||||||
|
max_color = COLORS.DISENGAGED
|
||||||
|
elif ui_state.status == UIStatus.OVERRIDE:
|
||||||
|
max_color = COLORS.OVERRIDE
|
||||||
|
|
||||||
|
max_text = tr("MAX")
|
||||||
|
max_text_width = measure_text_cached(self._font_semi_bold, max_text, FONT_SIZES.max_speed).x
|
||||||
|
rl.draw_text_ex(
|
||||||
|
self._font_semi_bold,
|
||||||
|
max_text,
|
||||||
|
rl.Vector2(rect.x + (rect.width - max_text_width) / 2, rect.y + 27),
|
||||||
|
FONT_SIZES.max_speed,
|
||||||
|
0,
|
||||||
|
max_color,
|
||||||
|
)
|
||||||
|
|
||||||
|
set_speed_text = (
|
||||||
|
CRUISE_DISABLED_CHAR
|
||||||
|
if not self.hud_renderer.is_cruise_set
|
||||||
|
else str(round(self.hud_renderer.set_speed))
|
||||||
|
)
|
||||||
|
speed_text_width = measure_text_cached(self._font_bold, set_speed_text, FONT_SIZES.set_speed).x
|
||||||
|
rl.draw_text_ex(
|
||||||
|
self._font_bold,
|
||||||
|
set_speed_text,
|
||||||
|
rl.Vector2(rect.x + (rect.width - speed_text_width) / 2, rect.y + 77),
|
||||||
|
FONT_SIZES.set_speed,
|
||||||
|
0,
|
||||||
|
set_speed_color,
|
||||||
|
)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import pyray as rl
|
||||||
|
from typing import Optional
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
|
from openpilot.selfdrive.ui.onroad.starpilot.widgets.base import LayoutWidget
|
||||||
|
from openpilot.selfdrive.ui.onroad.starpilot.slc_speed_limit import (
|
||||||
|
_get_slc_state, render_speed_limit_at, EU_SIGN_SIZE,
|
||||||
|
)
|
||||||
|
from openpilot.selfdrive.ui.onroad.starpilot.widget_style import CONTROL_WIDTH, SLC_HEIGHT
|
||||||
|
|
||||||
|
|
||||||
|
class SpeedLimitWidget(LayoutWidget):
|
||||||
|
TOUCH_SLOP = 20
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("speed_limit", priority=2)
|
||||||
|
self._slc_state: dict | None = None
|
||||||
|
self._sign_rect: Optional[rl.Rectangle] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _hit_rect(self) -> rl.Rectangle:
|
||||||
|
rect = self._sign_rect or self.rect
|
||||||
|
slop = self.TOUCH_SLOP
|
||||||
|
return rl.Rectangle(
|
||||||
|
rect.x - slop,
|
||||||
|
rect.y - slop,
|
||||||
|
rect.width + 2 * slop,
|
||||||
|
rect.height + 2 * slop,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_visible(self) -> bool:
|
||||||
|
self._slc_state = _get_slc_state()
|
||||||
|
if self._slc_state is None:
|
||||||
|
self._sign_rect = None
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_size(self) -> tuple[float, float]:
|
||||||
|
if self._slc_state is None:
|
||||||
|
return 0.0, 0.0
|
||||||
|
|
||||||
|
use_vienna = self._slc_state['use_vienna']
|
||||||
|
w = float(EU_SIGN_SIZE if use_vienna else CONTROL_WIDTH)
|
||||||
|
h = float(EU_SIGN_SIZE if use_vienna else SLC_HEIGHT)
|
||||||
|
|
||||||
|
return w, h
|
||||||
|
|
||||||
|
def _render(self, rect: rl.Rectangle) -> None:
|
||||||
|
if self._slc_state is None:
|
||||||
|
return
|
||||||
|
params = ui_state.ui_params
|
||||||
|
expanded = params.get_bool("SpeedLimitSources")
|
||||||
|
self._sign_rect = render_speed_limit_at(self._slc_state, rect, expanded)
|
||||||
|
|
||||||
|
def _handle_mouse_press(self, mouse_pos) -> None:
|
||||||
|
state = self._slc_state
|
||||||
|
if state is None or not rl.check_collision_point_rec(mouse_pos, self._hit_rect):
|
||||||
|
return
|
||||||
|
|
||||||
|
if state['speed_limit_changed'] and state['unconfirmed_valid']:
|
||||||
|
Params(memory=True).put_bool("SpeedLimitAccepted", True)
|
||||||
|
return
|
||||||
|
|
||||||
|
params = ui_state.ui_params
|
||||||
|
current = params.get_bool("SpeedLimitSources")
|
||||||
|
params.put_bool("SpeedLimitSources", not current)
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
"""One Big UI card for Max Set and the accepted speed limit."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import math
|
|
||||||
|
|
||||||
import pyray as rl
|
|
||||||
from openpilot.common.params import Params
|
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
|
||||||
from openpilot.selfdrive.ui.onroad.hud_renderer import COLORS
|
|
||||||
from openpilot.selfdrive.ui.onroad.starpilot.slc_speed_limit import (
|
|
||||||
_draw_source_icon, _draw_sources_bubble, _get_slc_state, _is_slc_enabled, _speed_limit_pulse_color, source_icon_key,
|
|
||||||
)
|
|
||||||
from openpilot.selfdrive.ui.onroad.starpilot.unified_speed_presentation import (
|
|
||||||
UnifiedSpeedPresentation, resolve_unified_speed,
|
|
||||||
)
|
|
||||||
from openpilot.selfdrive.ui.onroad.starpilot.widget_style import (
|
|
||||||
CONTROL_BG, CONTROL_ROUNDNESS, CONTROL_SEGMENTS, draw_control_card, roundness_for,
|
|
||||||
)
|
|
||||||
from openpilot.selfdrive.ui.onroad.starpilot.widgets.base import LayoutWidget
|
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
|
||||||
|
|
||||||
|
|
||||||
UNIFIED_WIDTH = 520
|
|
||||||
UNIFIED_HEIGHT = 250
|
|
||||||
SINGLE_WIDTH = 250
|
|
||||||
MERGED_SEPARATOR_Y = 76
|
|
||||||
HEADER_ICON_SIZE = 34
|
|
||||||
HEADER_FONT_SIZE = 28
|
|
||||||
VALUE_FONT_SIZE = 96
|
|
||||||
UNIT_FONT_SIZE = 28
|
|
||||||
PAUSE_ICON_WIDTH = 12
|
|
||||||
PAUSE_ICON_HEIGHT = 14
|
|
||||||
PAUSE_ICON_GAP = 8
|
|
||||||
OFFSET_FONT_SIZE = 22
|
|
||||||
OFFSET_PILL_HEIGHT = 30
|
|
||||||
CONFIRMATION_COLOR = rl.Color(188, 132, 255, 255)
|
|
||||||
UNIFIED_ACCENT = rl.Color(160, 96, 230, 230)
|
|
||||||
OFFSET_COLOR = rl.Color(UNIFIED_ACCENT.r, UNIFIED_ACCENT.g, UNIFIED_ACCENT.b, 255)
|
|
||||||
|
|
||||||
|
|
||||||
def _draw_header_icon(icon_key: str, x: float, y: float) -> None:
|
|
||||||
scale = max(1.0, gui_app._scale * max(gui_app._pixel_scale_x, gui_app._pixel_scale_y))
|
|
||||||
# Supersample for smooth edges.
|
|
||||||
texture_size = math.ceil(2 * HEADER_ICON_SIZE * scale)
|
|
||||||
|
|
||||||
def render() -> None:
|
|
||||||
rl.rl_push_matrix()
|
|
||||||
try:
|
|
||||||
texture_scale = texture_size / HEADER_ICON_SIZE
|
|
||||||
rl.rl_scalef(texture_scale, texture_scale, 1.0)
|
|
||||||
_draw_source_icon(icon_key, 0, 0, HEADER_ICON_SIZE, rl.WHITE)
|
|
||||||
finally:
|
|
||||||
rl.rl_pop_matrix()
|
|
||||||
|
|
||||||
texture = gui_app.cached_render_texture(
|
|
||||||
f"unified-speed-header:{icon_key}:{texture_size}", texture_size, texture_size, render,
|
|
||||||
)
|
|
||||||
if texture is None:
|
|
||||||
_draw_source_icon(icon_key, x, y, HEADER_ICON_SIZE, rl.WHITE)
|
|
||||||
return
|
|
||||||
|
|
||||||
rl.begin_blend_mode(rl.BlendMode.BLEND_ALPHA_PREMULTIPLY)
|
|
||||||
try:
|
|
||||||
rl.draw_texture_pro(
|
|
||||||
texture, rl.Rectangle(0, 0, texture_size, -texture_size),
|
|
||||||
rl.Rectangle(x, y, HEADER_ICON_SIZE, HEADER_ICON_SIZE), rl.Vector2(0, 0), 0.0, rl.WHITE,
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
rl.end_blend_mode()
|
|
||||||
|
|
||||||
|
|
||||||
class UnifiedSpeedWidget(LayoutWidget):
|
|
||||||
TOUCH_SLOP = 20
|
|
||||||
|
|
||||||
def __init__(self, hud_renderer):
|
|
||||||
super().__init__("unified_speed", priority=1)
|
|
||||||
self.hud_renderer = hud_renderer
|
|
||||||
self._font_semi_bold = gui_app.font(FontWeight.SEMI_BOLD)
|
|
||||||
self._font_bold = gui_app.font(FontWeight.BOLD)
|
|
||||||
self._slc_state: dict | None = None
|
|
||||||
self._slc_enabled = False
|
|
||||||
self._presentation: UnifiedSpeedPresentation | None = None
|
|
||||||
self._show_max = False
|
|
||||||
self._pedal_override = False
|
|
||||||
self._snapshot_frame: int | None = None
|
|
||||||
|
|
||||||
def _refresh_snapshot(self) -> None:
|
|
||||||
frame = getattr(ui_state.sm, "frame", None)
|
|
||||||
if frame is not None and frame == self._snapshot_frame:
|
|
||||||
return
|
|
||||||
self._snapshot_frame = frame
|
|
||||||
self._slc_enabled = _is_slc_enabled()
|
|
||||||
self._slc_state = _get_slc_state()
|
|
||||||
self._show_max = (
|
|
||||||
self.hud_renderer.is_cruise_available and
|
|
||||||
not ui_state.starpilot_toggles.get("hide_max_speed", False)
|
|
||||||
)
|
|
||||||
self._pedal_override = (
|
|
||||||
self.hud_renderer.is_cruise_set and ui_state.engaged and
|
|
||||||
ui_state.sm.valid.get("carState", False) and ui_state.sm.alive.get("carState", False) and
|
|
||||||
ui_state.sm.recv_frame["carState"] >= ui_state.started_frame and ui_state.sm["carState"].gasPressed
|
|
||||||
)
|
|
||||||
self._presentation = resolve_unified_speed(
|
|
||||||
self._show_max, self.hud_renderer.is_cruise_set, self.hud_renderer.set_speed,
|
|
||||||
self._slc_state, self._slc_enabled, ui_state.is_metric,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_visible(self) -> bool:
|
|
||||||
self._refresh_snapshot()
|
|
||||||
return self._show_max or self._presentation.mode != "max_only"
|
|
||||||
|
|
||||||
def get_size(self) -> tuple[float, float]:
|
|
||||||
self._refresh_snapshot()
|
|
||||||
width = UNIFIED_WIDTH if self._presentation.mode in ("split", "merged") else SINGLE_WIDTH
|
|
||||||
return float(width), float(UNIFIED_HEIGHT)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _hit_rect(self) -> rl.Rectangle:
|
|
||||||
rect = self.rect
|
|
||||||
return rl.Rectangle(
|
|
||||||
rect.x, rect.y - self.TOUCH_SLOP,
|
|
||||||
rect.width + self.TOUCH_SLOP, rect.height + 2 * self.TOUCH_SLOP,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _speed_limit_bounds(self, rect: rl.Rectangle) -> rl.Rectangle | None:
|
|
||||||
mode = self._presentation.mode
|
|
||||||
if mode in ("split", "merged"):
|
|
||||||
return rl.Rectangle(rect.x + rect.width / 2, rect.y, rect.width / 2, rect.height)
|
|
||||||
if mode == "limit_only":
|
|
||||||
return rect
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _draw_centered_text(self, text: str, bounds: rl.Rectangle, y: float,
|
|
||||||
font_size: int, color: rl.Color, *, bold: bool = False) -> None:
|
|
||||||
font = self._font_bold if bold else self._font_semi_bold
|
|
||||||
text_size = measure_text_cached(font, text, font_size)
|
|
||||||
text_x = bounds.x + (bounds.width - text_size.x) / 2
|
|
||||||
rl.draw_text_ex(font, text, rl.Vector2(text_x, y), font_size, 0, color)
|
|
||||||
|
|
||||||
def _draw_header(self, bounds: rl.Rectangle, text: str, icon_key: str | None, label_color: rl.Color) -> None:
|
|
||||||
text = tr(text)
|
|
||||||
font_size = HEADER_FONT_SIZE
|
|
||||||
icon_width = HEADER_ICON_SIZE + 9 if icon_key else 0
|
|
||||||
while font_size > 16 and measure_text_cached(self._font_semi_bold, text, font_size).x + icon_width > bounds.width - 24:
|
|
||||||
font_size -= 1
|
|
||||||
text_size = measure_text_cached(self._font_semi_bold, text, font_size)
|
|
||||||
group_width = icon_width + text_size.x
|
|
||||||
group_x = bounds.x + (bounds.width - group_width) / 2
|
|
||||||
icon_y = bounds.y + 20
|
|
||||||
if icon_key:
|
|
||||||
_draw_header_icon(icon_key, group_x, icon_y)
|
|
||||||
rl.draw_text_ex(
|
|
||||||
self._font_semi_bold, text,
|
|
||||||
rl.Vector2(group_x + icon_width, icon_y + (HEADER_ICON_SIZE - text_size.y) / 2),
|
|
||||||
font_size, 0, label_color,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _draw_offset_pill(self, bounds: rl.Rectangle, text: str, y: float) -> None:
|
|
||||||
text_size = measure_text_cached(self._font_semi_bold, text, OFFSET_FONT_SIZE)
|
|
||||||
width = max(56.0, text_size.x + 20.0)
|
|
||||||
pill = rl.Rectangle(bounds.x + (bounds.width - width) / 2, y, width, OFFSET_PILL_HEIGHT)
|
|
||||||
rl.draw_rectangle_rounded(pill, roundness_for(pill, 17), 8, rl.Color(32, 20, 45, 255))
|
|
||||||
rl.draw_rectangle_rounded_lines_ex(pill, roundness_for(pill, 17), 8, 2, OFFSET_COLOR)
|
|
||||||
self._draw_centered_text(text, pill, y + (pill.height - text_size.y) / 2, OFFSET_FONT_SIZE, OFFSET_COLOR)
|
|
||||||
|
|
||||||
def _draw_unit(self, bounds: rl.Rectangle, y: float) -> None:
|
|
||||||
text = tr(self._presentation.unit_text)
|
|
||||||
color = COLORS.WHITE_TRANSLUCENT
|
|
||||||
if self._pedal_override:
|
|
||||||
text_size = measure_text_cached(self._font_semi_bold, text, UNIT_FONT_SIZE)
|
|
||||||
text_shift = (PAUSE_ICON_WIDTH + PAUSE_ICON_GAP) / 2
|
|
||||||
icon_x = bounds.x + (bounds.width - text_size.x) / 2 - text_shift
|
|
||||||
icon_y = y + (text_size.y - PAUSE_ICON_HEIGHT) / 2
|
|
||||||
bar_width = PAUSE_ICON_WIDTH / 3
|
|
||||||
for x in (icon_x, icon_x + 2 * bar_width):
|
|
||||||
rl.draw_rectangle_rec(rl.Rectangle(x, icon_y, bar_width, PAUSE_ICON_HEIGHT), OFFSET_COLOR)
|
|
||||||
bounds = rl.Rectangle(bounds.x + text_shift, bounds.y, bounds.width, bounds.height)
|
|
||||||
color = COLORS.DISENGAGED
|
|
||||||
self._draw_centered_text(text, bounds, y, UNIT_FONT_SIZE, color)
|
|
||||||
|
|
||||||
def _max_header_color(self, active_side: str, cruise_set: bool) -> rl.Color:
|
|
||||||
if self._pedal_override:
|
|
||||||
return COLORS.DISENGAGED
|
|
||||||
if cruise_set and ui_state.status == UIStatus.ENGAGED and active_side in ("max", "shared"):
|
|
||||||
return COLORS.ENGAGED
|
|
||||||
if cruise_set and ui_state.status in (UIStatus.DISENGAGED, UIStatus.OVERRIDE):
|
|
||||||
return COLORS.DISENGAGED
|
|
||||||
return COLORS.GREY
|
|
||||||
|
|
||||||
def _limit_header_color(self, active_side: str, overridden: bool) -> rl.Color:
|
|
||||||
if self._pedal_override or overridden or ui_state.status in (UIStatus.DISENGAGED, UIStatus.OVERRIDE):
|
|
||||||
return COLORS.DISENGAGED
|
|
||||||
if ui_state.status == UIStatus.ENGAGED and active_side in ("slc", "shared"):
|
|
||||||
return COLORS.ENGAGED
|
|
||||||
return COLORS.GREY
|
|
||||||
|
|
||||||
def _draw_active_emphasis(self, rect: rl.Rectangle) -> None:
|
|
||||||
presentation = self._presentation
|
|
||||||
if self._pedal_override or presentation.mode == "merged" or ui_state.status != UIStatus.ENGAGED or presentation.active_side == "none":
|
|
||||||
return
|
|
||||||
if presentation.mode in ("max_only", "limit_only"):
|
|
||||||
bounds = rect
|
|
||||||
elif presentation.active_side == "slc":
|
|
||||||
bounds = self._speed_limit_bounds(rect)
|
|
||||||
elif presentation.active_side == "max":
|
|
||||||
bounds = rl.Rectangle(rect.x, rect.y, rect.width / 2, rect.height)
|
|
||||||
else:
|
|
||||||
bounds = rect
|
|
||||||
rl.draw_line_ex(
|
|
||||||
rl.Vector2(bounds.x + 18, rect.y + 65),
|
|
||||||
rl.Vector2(bounds.x + bounds.width - 18, rect.y + 65),
|
|
||||||
3, UNIFIED_ACCENT,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _draw_merged_separator(self, rect: rl.Rectangle) -> None:
|
|
||||||
center = rect.x + rect.width / 2
|
|
||||||
shelf_y = rect.y + MERGED_SEPARATOR_Y
|
|
||||||
valley_y = shelf_y + 12
|
|
||||||
color = rl.Color(UNIFIED_ACCENT.r, UNIFIED_ACCENT.g, UNIFIED_ACCENT.b, 170)
|
|
||||||
rl.draw_line_ex(rl.Vector2(rect.x + 18, shelf_y), rl.Vector2(center - 34, shelf_y), 2, color)
|
|
||||||
rl.draw_spline_segment_bezier_cubic(
|
|
||||||
rl.Vector2(center - 34, shelf_y), rl.Vector2(center - 19, shelf_y),
|
|
||||||
rl.Vector2(center - 23, valley_y), rl.Vector2(center - 7, valley_y), 2, color,
|
|
||||||
)
|
|
||||||
rl.draw_line_ex(rl.Vector2(center - 7, valley_y), rl.Vector2(center + 7, valley_y), 2, color)
|
|
||||||
rl.draw_spline_segment_bezier_cubic(
|
|
||||||
rl.Vector2(center + 7, valley_y), rl.Vector2(center + 23, valley_y),
|
|
||||||
rl.Vector2(center + 19, shelf_y), rl.Vector2(center + 34, shelf_y), 2, color,
|
|
||||||
)
|
|
||||||
rl.draw_line_ex(rl.Vector2(center + 34, shelf_y), rl.Vector2(rect.x + rect.width - 18, shelf_y), 2, color)
|
|
||||||
|
|
||||||
def _draw_speed_limit_border(self, rect: rl.Rectangle, right: rl.Rectangle, color: rl.Color) -> None:
|
|
||||||
# Clip the shared rounded outline so only the Speed Limit side changes.
|
|
||||||
rl.begin_scissor_mode(int(right.x), int(rect.y), int(right.width + 1), int(rect.height + 1))
|
|
||||||
try:
|
|
||||||
rl.draw_rectangle_rounded_lines_ex(rect, CONTROL_ROUNDNESS, CONTROL_SEGMENTS, 3, color)
|
|
||||||
finally:
|
|
||||||
rl.end_scissor_mode()
|
|
||||||
if self._presentation.mode == "split":
|
|
||||||
rl.draw_line_ex(rl.Vector2(right.x, rect.y + 8), rl.Vector2(right.x, rect.y + rect.height - 8), 3, color)
|
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle) -> None:
|
|
||||||
presentation = self._presentation
|
|
||||||
state = self._slc_state
|
|
||||||
speed_color = COLORS.DISENGAGED if self._pedal_override else COLORS.WHITE
|
|
||||||
rl.draw_rectangle_rounded_lines_ex(
|
|
||||||
rect, CONTROL_ROUNDNESS, CONTROL_SEGMENTS, 7,
|
|
||||||
rl.Color(UNIFIED_ACCENT.r, UNIFIED_ACCENT.g, UNIFIED_ACCENT.b, 55),
|
|
||||||
)
|
|
||||||
draw_control_card(rect, fill=CONTROL_BG, border=UNIFIED_ACCENT, border_width=2)
|
|
||||||
if presentation.mode == "split":
|
|
||||||
divider_x = rect.x + rect.width / 2
|
|
||||||
rl.draw_line_ex(
|
|
||||||
rl.Vector2(divider_x, rect.y + 8), rl.Vector2(divider_x, rect.y + rect.height - 8),
|
|
||||||
2, rl.Color(UNIFIED_ACCENT.r, UNIFIED_ACCENT.g, UNIFIED_ACCENT.b, 110),
|
|
||||||
)
|
|
||||||
elif presentation.mode == "merged":
|
|
||||||
self._draw_merged_separator(rect)
|
|
||||||
|
|
||||||
self._draw_active_emphasis(rect)
|
|
||||||
max_bounds = rl.Rectangle(rect.x, rect.y, rect.width / 2, rect.height) if presentation.mode in ("split", "merged") else rect
|
|
||||||
limit_bounds = self._speed_limit_bounds(rect)
|
|
||||||
if self._show_max or presentation.confirmation_pending:
|
|
||||||
max_color = COLORS.DARK_GREY if not self.hud_renderer.is_cruise_set else speed_color
|
|
||||||
max_label_color = self._max_header_color(presentation.active_side, self.hud_renderer.is_cruise_set)
|
|
||||||
self._draw_header(max_bounds, "MAX SET", "speedometer", max_label_color)
|
|
||||||
if presentation.mode != "merged":
|
|
||||||
self._draw_centered_text(presentation.max_speed_text, max_bounds, rect.y + 75, VALUE_FONT_SIZE, max_color, bold=True)
|
|
||||||
self._draw_unit(max_bounds, rect.y + 204)
|
|
||||||
|
|
||||||
if limit_bounds is not None:
|
|
||||||
icon_key = source_icon_key(presentation.source)
|
|
||||||
overridden = bool(state and state['slc_overridden_speed'])
|
|
||||||
label_color = self._limit_header_color(presentation.active_side, overridden)
|
|
||||||
self._draw_header(limit_bounds, "SPEED LIMIT", icon_key, label_color)
|
|
||||||
if presentation.mode != "merged":
|
|
||||||
self._draw_centered_text(presentation.posted_speed_text, limit_bounds, rect.y + 75, VALUE_FONT_SIZE, speed_color, bold=True)
|
|
||||||
if presentation.confirmation_pending:
|
|
||||||
self._draw_centered_text(tr("PENDING"), limit_bounds, rect.y + 175, 25, CONFIRMATION_COLOR)
|
|
||||||
elif presentation.offset_text is not None:
|
|
||||||
self._draw_offset_pill(limit_bounds, presentation.offset_text, rect.y + 175)
|
|
||||||
self._draw_unit(limit_bounds, rect.y + 204)
|
|
||||||
|
|
||||||
if presentation.mode == "merged":
|
|
||||||
self._draw_centered_text(presentation.effective_speed_text, rect, rect.y + 98, VALUE_FONT_SIZE, speed_color, bold=True)
|
|
||||||
self._draw_unit(rect, rect.y + 204)
|
|
||||||
if presentation.offset_text is not None:
|
|
||||||
self._draw_offset_pill(
|
|
||||||
limit_bounds, presentation.offset_text, rect.y + MERGED_SEPARATOR_Y - OFFSET_PILL_HEIGHT / 2,
|
|
||||||
)
|
|
||||||
|
|
||||||
if presentation.confirmation_pending and limit_bounds is not None:
|
|
||||||
intensity = (1.0 + math.sin(2.0 * math.pi * rl.get_time())) / 2.0
|
|
||||||
alpha = round(100 + 155 * intensity)
|
|
||||||
pulse = rl.Color(CONFIRMATION_COLOR.r, CONFIRMATION_COLOR.g, CONFIRMATION_COLOR.b, alpha)
|
|
||||||
self._draw_speed_limit_border(rect, limit_bounds, pulse)
|
|
||||||
else:
|
|
||||||
if limit_bounds is not None and state is not None:
|
|
||||||
vision_color = _speed_limit_pulse_color(UNIFIED_ACCENT, UNIFIED_ACCENT.a)
|
|
||||||
if (vision_color.r, vision_color.g, vision_color.b) != (UNIFIED_ACCENT.r, UNIFIED_ACCENT.g, UNIFIED_ACCENT.b):
|
|
||||||
self._draw_speed_limit_border(rect, limit_bounds, vision_color)
|
|
||||||
if state is not None and ui_state.ui_params.get_bool("SpeedLimitSources"):
|
|
||||||
_draw_sources_bubble(state, rect)
|
|
||||||
|
|
||||||
def _handle_mouse_press(self, mouse_pos) -> None:
|
|
||||||
right = self._speed_limit_bounds(self.rect)
|
|
||||||
if right is None and self._slc_state is not None:
|
|
||||||
# The detailed source panel remains dismissible when no limit is valid.
|
|
||||||
right = self.rect
|
|
||||||
if right is None:
|
|
||||||
return
|
|
||||||
target = rl.Rectangle(right.x, right.y - self.TOUCH_SLOP,
|
|
||||||
right.width + self.TOUCH_SLOP, right.height + 2 * self.TOUCH_SLOP)
|
|
||||||
if not rl.check_collision_point_rec(mouse_pos, target):
|
|
||||||
return
|
|
||||||
if self._presentation.confirmation_pending:
|
|
||||||
Params(memory=True).put_bool("SpeedLimitAccepted", True)
|
|
||||||
return
|
|
||||||
params = ui_state.ui_params
|
|
||||||
params.put_bool("SpeedLimitSources", not params.get_bool("SpeedLimitSources"))
|
|
||||||
@@ -69,7 +69,8 @@ def _load_starpilot_onroad_view(monkeypatch):
|
|||||||
stub_module("openpilot.selfdrive.ui.onroad.starpilot.widget_layout_manager", WidgetLayoutManager=dummy_widget)
|
stub_module("openpilot.selfdrive.ui.onroad.starpilot.widget_layout_manager", WidgetLayoutManager=dummy_widget)
|
||||||
stub_module(
|
stub_module(
|
||||||
"openpilot.selfdrive.ui.onroad.starpilot.widgets",
|
"openpilot.selfdrive.ui.onroad.starpilot.widgets",
|
||||||
UnifiedSpeedWidget=dummy_widget,
|
SetSpeedWidget=dummy_widget,
|
||||||
|
SpeedLimitWidget=dummy_widget,
|
||||||
PedalIconsWidget=dummy_widget,
|
PedalIconsWidget=dummy_widget,
|
||||||
AetherGaugeWidget=dummy_widget,
|
AetherGaugeWidget=dummy_widget,
|
||||||
PersonalityButtonWidget=dummy_widget,
|
PersonalityButtonWidget=dummy_widget,
|
||||||
|
|||||||
@@ -80,19 +80,39 @@ def test_visible_source_rows_honor_active_only_and_source_order():
|
|||||||
]
|
]
|
||||||
# When no sources have a valid speed reading (> 0), returns empty list (triggers empty state)
|
# When no sources have a valid speed reading (> 0), returns empty list (triggers empty state)
|
||||||
assert visible_source_rows(
|
assert visible_source_rows(
|
||||||
source_defs, dict.fromkeys(values, 0.0), "Map Data", ("Map Data",),
|
source_defs, {key: 0.0 for key in values}, "Map Data", ("Map Data",),
|
||||||
) == []
|
) == []
|
||||||
|
|
||||||
|
|
||||||
def test_header_reuses_diagnostic_source_icons_without_unknown_fallback():
|
def test_source_label_color_override_and_engagement_states():
|
||||||
from openpilot.selfdrive.ui.onroad.starpilot.slc_speed_limit import source_icon_key
|
from openpilot.selfdrive.ui.onroad.starpilot.slc_speed_limit import _source_label_color
|
||||||
|
from openpilot.selfdrive.ui.onroad.hud_renderer import COLORS
|
||||||
|
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||||
|
|
||||||
assert source_icon_key("Vision") == "camera"
|
# Engaged and not overridden -> Active green
|
||||||
assert source_icon_key("Dashboard") == "dashboard"
|
ui_state.status = UIStatus.ENGAGED
|
||||||
assert source_icon_key("Map Data") == "map"
|
color = _source_label_color(255, is_overridden=False)
|
||||||
assert source_icon_key("Mapbox") == "map"
|
assert (color.r, color.g, color.b, color.a) == (COLORS.ENGAGED.r, COLORS.ENGAGED.g, COLORS.ENGAGED.b, 255)
|
||||||
assert source_icon_key("None") is None
|
|
||||||
assert source_icon_key("Unexpected") is None
|
# Engaged but overridden -> Disengaged/override gray
|
||||||
|
color_overridden = _source_label_color(255, is_overridden=True)
|
||||||
|
assert (color_overridden.r, color_overridden.g, color_overridden.b, color_overridden.a) == (
|
||||||
|
COLORS.DISENGAGED.r, COLORS.DISENGAGED.g, COLORS.DISENGAGED.b, 255
|
||||||
|
)
|
||||||
|
|
||||||
|
# Disengaged -> Disengaged/override gray
|
||||||
|
ui_state.status = UIStatus.DISENGAGED
|
||||||
|
color_disengaged = _source_label_color(255, is_overridden=False)
|
||||||
|
assert (color_disengaged.r, color_disengaged.g, color_disengaged.b, color_disengaged.a) == (
|
||||||
|
COLORS.DISENGAGED.r, COLORS.DISENGAGED.g, COLORS.DISENGAGED.b, 255
|
||||||
|
)
|
||||||
|
|
||||||
|
# Override UI status -> Disengaged/override gray
|
||||||
|
ui_state.status = UIStatus.OVERRIDE
|
||||||
|
color_ui_override = _source_label_color(255, is_overridden=False)
|
||||||
|
assert (color_ui_override.r, color_ui_override.g, color_ui_override.b, color_ui_override.a) == (
|
||||||
|
COLORS.OVERRIDE.r, COLORS.OVERRIDE.g, COLORS.OVERRIDE.b, 255
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_vision_pulse_ignores_same_limit_source_flapping(monkeypatch):
|
def test_vision_pulse_ignores_same_limit_source_flapping(monkeypatch):
|
||||||
|
|||||||
@@ -1,186 +0,0 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
from openpilot.common.constants import CV
|
|
||||||
from openpilot.selfdrive.ui.onroad.starpilot.unified_speed_presentation import resolve_unified_speed
|
|
||||||
|
|
||||||
|
|
||||||
def slc_state(posted_mph=65, offset_mph=0, source="Map Data", *, pending_mph=0,
|
|
||||||
enabled=True, limiting=False, overridden=False, metric=False):
|
|
||||||
conversion = CV.MS_TO_KPH if metric else CV.MS_TO_MPH
|
|
||||||
return {
|
|
||||||
"accepted_speed_limit_ms": posted_mph / conversion,
|
|
||||||
"effective_target_ms": max(0, posted_mph + offset_mph) / conversion,
|
|
||||||
"offset_ms": offset_mph / conversion,
|
|
||||||
"speed_conversion": conversion,
|
|
||||||
"unconfirmed_speed_limit": pending_mph,
|
|
||||||
"unconfirmed_valid": pending_mph > 0,
|
|
||||||
"speed_limit_changed": pending_mph > 0,
|
|
||||||
"presented_source": source,
|
|
||||||
"slc_enabled": enabled,
|
|
||||||
"slc_is_limiting_max_set": limiting,
|
|
||||||
"slc_overridden_speed": 1.0 if overridden else 0.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("max_speed,posted,offset,expected_mode", [
|
|
||||||
(80, 65, 5, "split"),
|
|
||||||
(70, 70, 0, "merged"),
|
|
||||||
(70, 65, 5, "merged"),
|
|
||||||
(70, 65, 4, "split"),
|
|
||||||
(65, 70, -5, "merged"),
|
|
||||||
])
|
|
||||||
def test_split_and_merge_use_effective_accepted_limit(max_speed, posted, offset, expected_mode):
|
|
||||||
result = resolve_unified_speed(True, True, max_speed, slc_state(posted, offset), True, False)
|
|
||||||
assert result.mode == expected_mode
|
|
||||||
assert result.posted_speed_text == str(posted)
|
|
||||||
assert result.effective_speed_text == str(posted + offset)
|
|
||||||
assert result.offset_text == (f"{offset:+d}" if offset else None)
|
|
||||||
|
|
||||||
|
|
||||||
def test_pending_candidate_forces_split_without_replacing_accepted_target():
|
|
||||||
state = slc_state(65, 5, source="Vision", pending_mph=75)
|
|
||||||
result = resolve_unified_speed(True, True, 70, state, True, False)
|
|
||||||
assert result.mode == "split"
|
|
||||||
assert result.confirmation_pending
|
|
||||||
assert result.posted_speed_text == "75"
|
|
||||||
assert result.effective_speed_text == "70"
|
|
||||||
assert result.source == "Vision"
|
|
||||||
|
|
||||||
|
|
||||||
def test_resolution_after_confirmation_uses_same_equality_rule():
|
|
||||||
state = slc_state(65, 5, pending_mph=75)
|
|
||||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "split"
|
|
||||||
state["speed_limit_changed"] = state["unconfirmed_valid"] = False
|
|
||||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "merged"
|
|
||||||
assert resolve_unified_speed(True, True, 80, state, True, False).mode == "split"
|
|
||||||
|
|
||||||
|
|
||||||
def test_source_change_and_override_do_not_change_layout():
|
|
||||||
state = slc_state(65, 5)
|
|
||||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "merged"
|
|
||||||
state["presented_source"] = "Vision"
|
|
||||||
result = resolve_unified_speed(True, True, 70, state, True, False)
|
|
||||||
assert result.mode == "merged"
|
|
||||||
assert result.source == "Vision"
|
|
||||||
state["presented_source"] = "Dashboard"
|
|
||||||
state["slc_overridden_speed"] = 40.0
|
|
||||||
result = resolve_unified_speed(True, True, 70, state, True, False)
|
|
||||||
assert result.mode == "merged"
|
|
||||||
assert result.source == "Dashboard"
|
|
||||||
|
|
||||||
|
|
||||||
def test_source_target_change_recomputes_layout_independently():
|
|
||||||
state = slc_state(65, 5, source="Map Data")
|
|
||||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "merged"
|
|
||||||
state.update(slc_state(55, 5, source="Vision"))
|
|
||||||
result = resolve_unified_speed(True, True, 70, state, True, False)
|
|
||||||
assert result.mode == "split"
|
|
||||||
assert result.source == "Vision"
|
|
||||||
|
|
||||||
|
|
||||||
def test_active_side_uses_published_control_semantic():
|
|
||||||
state = slc_state(65, 5, limiting=True)
|
|
||||||
assert resolve_unified_speed(True, True, 80, state, True, False).active_side == "slc"
|
|
||||||
state["slc_is_limiting_max_set"] = False
|
|
||||||
assert resolve_unified_speed(True, True, 80, state, True, False).active_side == "max"
|
|
||||||
state["slc_overridden_speed"] = 40.0
|
|
||||||
assert resolve_unified_speed(True, True, 80, state, True, False).active_side == "none"
|
|
||||||
|
|
||||||
|
|
||||||
def test_display_only_speed_limit_stays_split():
|
|
||||||
result = resolve_unified_speed(True, True, 70, slc_state(70, enabled=False), False, False)
|
|
||||||
assert result.mode == "split"
|
|
||||||
|
|
||||||
|
|
||||||
def test_disabled_confirmation_does_not_force_split():
|
|
||||||
state = slc_state(65, 5, pending_mph=75)
|
|
||||||
state["speed_limit_changed"] = False
|
|
||||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "merged"
|
|
||||||
|
|
||||||
|
|
||||||
def test_missing_limit_never_renders_zero_or_a_stale_source():
|
|
||||||
result = resolve_unified_speed(True, True, 70, slc_state(0, source="None"), True, False)
|
|
||||||
assert result.mode == "split"
|
|
||||||
assert result.posted_speed_text == "–"
|
|
||||||
assert result.effective_speed_text == "–"
|
|
||||||
assert result.source == "None"
|
|
||||||
|
|
||||||
|
|
||||||
def test_missing_limit_with_slc_disabled_allows_max_only():
|
|
||||||
result = resolve_unified_speed(True, True, 70, slc_state(0, source="None", enabled=False), False, False)
|
|
||||||
assert result.mode == "max_only"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("show_max,expected_mode", [(True, "split"), (False, "limit_only")])
|
|
||||||
def test_stale_slc_data_keeps_speed_limit_region(show_max, expected_mode):
|
|
||||||
result = resolve_unified_speed(show_max, True, 70, None, True, False)
|
|
||||||
assert result.mode == expected_mode
|
|
||||||
assert result.posted_speed_text == "–"
|
|
||||||
assert result.effective_speed_text == "–"
|
|
||||||
assert result.source == "None"
|
|
||||||
|
|
||||||
|
|
||||||
def test_missing_data_with_slc_disabled_uses_max_only():
|
|
||||||
assert resolve_unified_speed(True, True, 70, None, False, False).mode == "max_only"
|
|
||||||
|
|
||||||
|
|
||||||
def test_persisted_previous_limit_without_source_remains_visible():
|
|
||||||
result = resolve_unified_speed(True, True, 70, slc_state(45, source="Previous Limit"), True, False)
|
|
||||||
assert result.mode == "split"
|
|
||||||
assert result.posted_speed_text == "45"
|
|
||||||
assert result.source == "Previous Limit"
|
|
||||||
|
|
||||||
|
|
||||||
def test_low_limit_with_large_negative_offset_preserves_configured_offset():
|
|
||||||
result = resolve_unified_speed(True, True, 70, slc_state(5, -99), True, False)
|
|
||||||
assert result.mode == "split"
|
|
||||||
assert result.posted_speed_text == "5"
|
|
||||||
assert result.effective_speed_text == "–"
|
|
||||||
assert result.offset_text == "-99"
|
|
||||||
|
|
||||||
|
|
||||||
def test_metric_and_rounding_follow_the_displayed_value():
|
|
||||||
state = slc_state(65.4, 4.4, metric=True)
|
|
||||||
result = resolve_unified_speed(True, True, 70, state, True, True)
|
|
||||||
assert result.mode == "merged"
|
|
||||||
assert result.posted_speed_text == "65"
|
|
||||||
assert result.offset_text == "+4"
|
|
||||||
assert result.unit_text == "km/h"
|
|
||||||
|
|
||||||
|
|
||||||
def test_invisible_fraction_does_not_keep_card_split():
|
|
||||||
state = slc_state(65.1, 5.2)
|
|
||||||
result = resolve_unified_speed(True, True, 70.4, state, True, False)
|
|
||||||
assert result.mode == "merged"
|
|
||||||
|
|
||||||
|
|
||||||
def test_hidden_max_still_shows_posted_limit():
|
|
||||||
result = resolve_unified_speed(False, True, 70, slc_state(65), True, False)
|
|
||||||
assert result.mode == "limit_only"
|
|
||||||
result = resolve_unified_speed(False, True, 70, slc_state(65, enabled=False), False, False)
|
|
||||||
assert result.mode == "limit_only"
|
|
||||||
|
|
||||||
|
|
||||||
def test_confirmation_forces_split_even_when_max_is_hidden():
|
|
||||||
result = resolve_unified_speed(False, True, 70, slc_state(65, pending_mph=75), True, False)
|
|
||||||
assert result.mode == "split"
|
|
||||||
assert result.max_speed_text == "70"
|
|
||||||
assert result.confirmation_pending
|
|
||||||
|
|
||||||
|
|
||||||
def test_offset_max_pending_and_source_transitions_recompute_mode():
|
|
||||||
state = slc_state(65)
|
|
||||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "split"
|
|
||||||
state.update(slc_state(65, 5))
|
|
||||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "merged"
|
|
||||||
assert resolve_unified_speed(True, True, 75, state, True, False).mode == "split"
|
|
||||||
state.update(slc_state(65, 5, pending_mph=75))
|
|
||||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "split"
|
|
||||||
state.update(slc_state(65, 5))
|
|
||||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "merged"
|
|
||||||
state.update(slc_state(0, source="None"))
|
|
||||||
missing = resolve_unified_speed(True, True, 70, state, True, False)
|
|
||||||
assert missing.mode == "split"
|
|
||||||
assert missing.posted_speed_text == "–"
|
|
||||||
state.update(slc_state(65, 5))
|
|
||||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "merged"
|
|
||||||
@@ -1,495 +0,0 @@
|
|||||||
from types import SimpleNamespace
|
|
||||||
from dataclasses import replace
|
|
||||||
|
|
||||||
import pyray as rl
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from cereal import custom
|
|
||||||
from openpilot.common.constants import CV
|
|
||||||
from openpilot.selfdrive.ui.onroad.starpilot import slc_speed_limit
|
|
||||||
from openpilot.selfdrive.ui.onroad.starpilot.unified_speed_presentation import UnifiedSpeedPresentation, resolve_unified_speed
|
|
||||||
from openpilot.selfdrive.ui.onroad.starpilot.widgets import unified_speed
|
|
||||||
|
|
||||||
|
|
||||||
def make_widget(mode="split", pending=False):
|
|
||||||
widget = object.__new__(unified_speed.UnifiedSpeedWidget)
|
|
||||||
widget._rect = rl.Rectangle(30, 75, 520, 250)
|
|
||||||
widget._presentation = UnifiedSpeedPresentation(mode, "70", "65", "70", "+5", "mph", "Map Data", pending, "slc")
|
|
||||||
widget._show_max = True
|
|
||||||
widget._slc_state = None
|
|
||||||
widget._pedal_override = False
|
|
||||||
widget.hud_renderer = SimpleNamespace(is_cruise_set=True)
|
|
||||||
return widget
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def header_icon_cache(monkeypatch):
|
|
||||||
app = object.__new__(type(unified_speed.gui_app))
|
|
||||||
app._scale = app._pixel_scale_x = app._pixel_scale_y = 1.0
|
|
||||||
app._cached_render_textures = {}
|
|
||||||
app._pending_render_textures = {}
|
|
||||||
geometry, draws, allocations, scales = [], [], [], []
|
|
||||||
monkeypatch.setattr(unified_speed, "gui_app", app)
|
|
||||||
monkeypatch.setattr(unified_speed, "_draw_source_icon", lambda *args: geometry.append(args))
|
|
||||||
monkeypatch.setattr(unified_speed, "measure_text_cached", lambda *args: rl.Vector2(100, 28))
|
|
||||||
monkeypatch.setattr(rl, "draw_text_ex", lambda *args: None)
|
|
||||||
monkeypatch.setattr(rl, "draw_texture_pro", lambda *args: draws.append(args))
|
|
||||||
monkeypatch.setattr(rl, "rl_scalef", lambda *args: scales.append(args))
|
|
||||||
for name in ("rl_push_matrix", "rl_pop_matrix", "begin_texture_mode", "end_texture_mode", "clear_background",
|
|
||||||
"rl_set_blend_factors_separate", "begin_blend_mode", "end_blend_mode", "set_texture_filter", "set_texture_wrap"):
|
|
||||||
monkeypatch.setattr(rl, name, lambda *args: None)
|
|
||||||
|
|
||||||
def allocate(width, height):
|
|
||||||
allocations.append((width, height))
|
|
||||||
return SimpleNamespace(texture=SimpleNamespace(width=width, height=height))
|
|
||||||
|
|
||||||
monkeypatch.setattr(rl, "load_render_texture", allocate)
|
|
||||||
return app, geometry, draws, allocations, scales
|
|
||||||
|
|
||||||
|
|
||||||
def test_header_glyph_cache_is_shared_and_skips_geometry_after_first_frame(header_icon_cache):
|
|
||||||
app, geometry, draws, allocations, _scales = header_icon_cache
|
|
||||||
widgets = [make_widget(), make_widget()]
|
|
||||||
for widget in widgets:
|
|
||||||
widget._font_semi_bold = None
|
|
||||||
widget = widgets[0]
|
|
||||||
for label, icon in (("MAX SET", "speedometer"), ("SPEED LIMIT", "map")):
|
|
||||||
widget._draw_header(widget.rect, label, icon, rl.WHITE)
|
|
||||||
assert len(geometry) == 2
|
|
||||||
assert allocations == []
|
|
||||||
app._populate_render_texture_cache()
|
|
||||||
assert len(geometry) == 4
|
|
||||||
|
|
||||||
for frame in range(60):
|
|
||||||
widget = widgets[frame % 2]
|
|
||||||
bounds = rl.Rectangle(frame, frame, 260, 250)
|
|
||||||
widget._draw_header(bounds, "MAX SET", "speedometer", rl.WHITE)
|
|
||||||
widget._draw_header(bounds, f"LIMIT {frame}", "map", rl.GRAY)
|
|
||||||
assert len(geometry) == 4
|
|
||||||
assert len(draws) == 120
|
|
||||||
assert len(allocations) == len(app._cached_render_textures) == 2
|
|
||||||
assert app._pending_render_textures == {}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("scale,dpi,texture_size", [(0.5, 1.0, 68), (1.0, 2.0, 136), (1.25, 1.5, 128)])
|
|
||||||
def test_header_cache_resolution_preserves_logical_geometry(header_icon_cache, scale, dpi, texture_size):
|
|
||||||
app, geometry, draws, allocations, scales = header_icon_cache
|
|
||||||
app._scale, app._pixel_scale_x = scale, dpi
|
|
||||||
for icon in ("speedometer", "map", "camera", "dashboard", "next"):
|
|
||||||
unified_speed._draw_header_icon(icon, 10, 20)
|
|
||||||
app._populate_render_texture_cache()
|
|
||||||
assert len(app._cached_render_textures) == 5
|
|
||||||
assert allocations == [(texture_size, texture_size)] * 5
|
|
||||||
assert all(args[1:4] == (0, 0, 34) for args in geometry[5:])
|
|
||||||
assert scales == [(texture_size / 34, texture_size / 34, 1.0)] * 5
|
|
||||||
|
|
||||||
unified_speed._draw_header_icon("map", 200, 300)
|
|
||||||
assert len(geometry) == 10
|
|
||||||
source, destination = draws[-1][1:3]
|
|
||||||
assert (source.width, source.height) == (texture_size, -texture_size)
|
|
||||||
assert (destination.x, destination.y, destination.width, destination.height) == (200, 300, 34, 34)
|
|
||||||
|
|
||||||
|
|
||||||
def test_speed_limit_hit_target_is_right_half_in_both_layouts():
|
|
||||||
for mode in ("split", "merged"):
|
|
||||||
right = make_widget(mode)._speed_limit_bounds(rl.Rectangle(30, 75, 520, 250))
|
|
||||||
assert (right.x, right.width) == (290, 260)
|
|
||||||
|
|
||||||
|
|
||||||
def test_confirmation_touch_only_accepts_on_speed_limit_side(monkeypatch):
|
|
||||||
widget = make_widget(pending=True)
|
|
||||||
writes = []
|
|
||||||
monkeypatch.setattr(unified_speed, "Params", lambda memory: SimpleNamespace(put_bool=lambda key, value: writes.append((key, value))))
|
|
||||||
widget._handle_mouse_press(rl.Vector2(100, 150))
|
|
||||||
assert writes == []
|
|
||||||
widget._handle_mouse_press(rl.Vector2(400, 150))
|
|
||||||
assert writes == [("SpeedLimitAccepted", True)]
|
|
||||||
|
|
||||||
|
|
||||||
def test_merged_speed_limit_side_toggles_sources(monkeypatch):
|
|
||||||
widget = make_widget("merged")
|
|
||||||
writes = []
|
|
||||||
params = SimpleNamespace(get_bool=lambda _key: False, put_bool=lambda key, value: writes.append((key, value)))
|
|
||||||
monkeypatch.setattr(unified_speed, "ui_state", SimpleNamespace(ui_params=params))
|
|
||||||
widget._handle_mouse_press(rl.Vector2(100, 150))
|
|
||||||
assert writes == []
|
|
||||||
widget._handle_mouse_press(rl.Vector2(400, 150))
|
|
||||||
assert writes == [("SpeedLimitSources", True)]
|
|
||||||
|
|
||||||
|
|
||||||
def test_diagnostic_sources_can_be_dismissed_from_max_only_card(monkeypatch):
|
|
||||||
widget = make_widget("max_only")
|
|
||||||
widget._slc_state = {}
|
|
||||||
params = SimpleNamespace(get_bool=lambda _key: True, put_bool=lambda key, value: writes.append((key, value)))
|
|
||||||
writes = []
|
|
||||||
monkeypatch.setattr(unified_speed, "ui_state", SimpleNamespace(ui_params=params))
|
|
||||||
widget._handle_mouse_press(rl.Vector2(100, 150))
|
|
||||||
assert writes == [("SpeedLimitSources", False)]
|
|
||||||
|
|
||||||
|
|
||||||
def test_right_border_overlay_is_clipped_to_speed_limit_side(monkeypatch):
|
|
||||||
events = []
|
|
||||||
monkeypatch.setattr(unified_speed.rl, "begin_scissor_mode", lambda *args: events.append(("begin", args)))
|
|
||||||
monkeypatch.setattr(unified_speed.rl, "draw_rectangle_rounded_lines_ex", lambda *args: events.append(("outline", args)))
|
|
||||||
monkeypatch.setattr(unified_speed.rl, "draw_line_ex", lambda *args: events.append(("divider", args)))
|
|
||||||
monkeypatch.setattr(unified_speed.rl, "end_scissor_mode", lambda: events.append(("end",)))
|
|
||||||
for mode, expected in (("split", ["begin", "outline", "end", "divider"]),
|
|
||||||
("merged", ["begin", "outline", "end"])):
|
|
||||||
events.clear()
|
|
||||||
widget = make_widget(mode)
|
|
||||||
rect = widget.rect
|
|
||||||
right = widget._speed_limit_bounds(rect)
|
|
||||||
widget._draw_speed_limit_border(rect, right, rl.Color(188, 132, 255, 200))
|
|
||||||
assert events[0] == ("begin", (290, 75, 261, 251))
|
|
||||||
assert [event[0] for event in events] == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_split_and_merged_draw_one_card_with_both_headers(monkeypatch):
|
|
||||||
cards = []
|
|
||||||
lines = []
|
|
||||||
monkeypatch.setattr(unified_speed, "draw_control_card", lambda *args, **kwargs: cards.append(args[0]))
|
|
||||||
monkeypatch.setattr(unified_speed, "ui_state", SimpleNamespace(status=unified_speed.UIStatus.DISENGAGED,
|
|
||||||
ui_params=SimpleNamespace(get_bool=lambda _key: False)))
|
|
||||||
monkeypatch.setattr(unified_speed.rl, "draw_line_ex", lambda *args: lines.append(args))
|
|
||||||
monkeypatch.setattr(unified_speed.rl, "draw_rectangle_rounded_lines_ex", lambda *args: None)
|
|
||||||
for mode in ("split", "merged"):
|
|
||||||
lines.clear()
|
|
||||||
widget = make_widget(mode)
|
|
||||||
headers = []
|
|
||||||
values = []
|
|
||||||
separators = []
|
|
||||||
offsets = []
|
|
||||||
monkeypatch.setattr(widget, "_draw_header", lambda _bounds, text, icon, _color, rows=headers: rows.append((text, icon)))
|
|
||||||
monkeypatch.setattr(widget, "_draw_centered_text", lambda text, *args, rows=values, **kwargs: rows.append(text))
|
|
||||||
monkeypatch.setattr(widget, "_draw_offset_pill", lambda bounds, text, y, rows=offsets: rows.append((bounds, text, y)))
|
|
||||||
monkeypatch.setattr(widget, "_draw_merged_separator", lambda _rect, rows=separators: rows.append(True))
|
|
||||||
monkeypatch.setattr(widget, "_draw_active_emphasis", lambda *args: None)
|
|
||||||
widget._render(widget.rect)
|
|
||||||
assert headers == [("MAX SET", "speedometer"), ("SPEED LIMIT", "map")]
|
|
||||||
assert separators == ([True] if mode == "merged" else [])
|
|
||||||
assert sum(line[0].x == line[1].x == 290 for line in lines) == (1 if mode == "split" else 0)
|
|
||||||
assert values == (["70", "mph"] if mode == "merged" else ["70", "mph", "65", "mph"])
|
|
||||||
assert offsets[0][0].x == 290
|
|
||||||
assert offsets[0][2] == (136 if mode == "merged" else 250)
|
|
||||||
assert len(cards) == 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_merged_draws_effective_speed_once_and_skips_active_line(monkeypatch):
|
|
||||||
widget = make_widget("merged")
|
|
||||||
widget._presentation = replace(widget._presentation, max_speed_text="71", effective_speed_text="70", active_side="shared")
|
|
||||||
monkeypatch.setattr(unified_speed, "ui_state", SimpleNamespace(status=unified_speed.UIStatus.ENGAGED,
|
|
||||||
ui_params=SimpleNamespace(get_bool=lambda _key: False)))
|
|
||||||
monkeypatch.setattr(unified_speed, "draw_control_card", lambda *args, **kwargs: None)
|
|
||||||
monkeypatch.setattr(unified_speed.rl, "draw_rectangle_rounded_lines_ex", lambda *args: None)
|
|
||||||
lines = []
|
|
||||||
monkeypatch.setattr(unified_speed.rl, "draw_line_ex", lambda *args: lines.append(args))
|
|
||||||
monkeypatch.setattr(widget, "_draw_merged_separator", lambda _rect: None)
|
|
||||||
monkeypatch.setattr(widget, "_draw_header", lambda *args: None)
|
|
||||||
monkeypatch.setattr(widget, "_draw_offset_pill", lambda *args: None)
|
|
||||||
values = []
|
|
||||||
monkeypatch.setattr(widget, "_draw_centered_text", lambda text, *args, **kwargs: values.append(text))
|
|
||||||
widget._render(widget.rect)
|
|
||||||
assert values == ["70", "mph"]
|
|
||||||
assert lines == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_merged_separator_has_shallow_center_dip(monkeypatch):
|
|
||||||
widget = make_widget("merged")
|
|
||||||
segments = []
|
|
||||||
monkeypatch.setattr(unified_speed.rl, "draw_line_ex", lambda *args: segments.append(("line", args)))
|
|
||||||
monkeypatch.setattr(unified_speed.rl, "draw_spline_segment_bezier_cubic", lambda *args: segments.append(("curve", args)))
|
|
||||||
widget._draw_merged_separator(widget.rect)
|
|
||||||
assert [segment[0] for segment in segments] == ["line", "curve", "line", "curve", "line"]
|
|
||||||
assert segments[0][1][0].y == widget.rect.y + 76
|
|
||||||
assert segments[2][1][0].y == widget.rect.y + 88
|
|
||||||
|
|
||||||
|
|
||||||
def test_enabled_slc_stays_full_width_when_plan_is_stale(monkeypatch):
|
|
||||||
widget = make_widget("split")
|
|
||||||
widget._snapshot_frame = None
|
|
||||||
widget.hud_renderer = SimpleNamespace(is_cruise_available=True, is_cruise_set=True, set_speed=70)
|
|
||||||
monkeypatch.setattr(unified_speed, "ui_state", SimpleNamespace(
|
|
||||||
sm=SimpleNamespace(frame=1), starpilot_toggles={}, is_metric=False, engaged=False,
|
|
||||||
))
|
|
||||||
monkeypatch.setattr(unified_speed, "_is_slc_enabled", lambda: True)
|
|
||||||
monkeypatch.setattr(unified_speed, "_get_slc_state", lambda: None)
|
|
||||||
assert widget.get_size() == (520.0, 250.0)
|
|
||||||
assert widget.is_visible
|
|
||||||
assert widget._presentation.posted_speed_text == "–"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def pedal_snapshot(monkeypatch):
|
|
||||||
class SubMaster(dict):
|
|
||||||
pass
|
|
||||||
|
|
||||||
sm = SubMaster(carState=SimpleNamespace(gasPressed=True))
|
|
||||||
sm.frame = 20
|
|
||||||
sm.valid = {"carState": True}
|
|
||||||
sm.alive = {"carState": True}
|
|
||||||
sm.recv_frame = {"carState": 20}
|
|
||||||
ui = SimpleNamespace(sm=sm, started_frame=10, engaged=True, starpilot_toggles={}, is_metric=False)
|
|
||||||
widget = make_widget()
|
|
||||||
widget._snapshot_frame = None
|
|
||||||
widget.hud_renderer = SimpleNamespace(is_cruise_available=True, is_cruise_set=True, set_speed=70)
|
|
||||||
monkeypatch.setattr(unified_speed, "ui_state", ui)
|
|
||||||
monkeypatch.setattr(unified_speed, "_is_slc_enabled", lambda: True)
|
|
||||||
monkeypatch.setattr(unified_speed, "_get_slc_state", lambda: None)
|
|
||||||
return widget, ui
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("gas,engaged,cruise_set,valid,alive,received,expected", [
|
|
||||||
(True, True, True, True, True, 20, True),
|
|
||||||
(False, True, True, True, True, 20, False),
|
|
||||||
(True, False, True, True, True, 20, False),
|
|
||||||
(True, True, False, True, True, 20, False),
|
|
||||||
(True, True, True, False, True, 20, False),
|
|
||||||
(True, True, True, True, False, 20, False),
|
|
||||||
(True, True, True, True, True, 9, False),
|
|
||||||
])
|
|
||||||
def test_pedal_override_requires_fresh_gas_and_engaged_cruise(pedal_snapshot, gas, engaged, cruise_set, valid, alive, received, expected):
|
|
||||||
widget, ui = pedal_snapshot
|
|
||||||
ui.sm["carState"].gasPressed = gas
|
|
||||||
ui.engaged = engaged
|
|
||||||
widget.hud_renderer.is_cruise_set = cruise_set
|
|
||||||
ui.sm.valid["carState"] = valid
|
|
||||||
ui.sm.alive["carState"] = alive
|
|
||||||
ui.sm.recv_frame["carState"] = received
|
|
||||||
widget._refresh_snapshot()
|
|
||||||
assert widget._pedal_override == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_pedal_cue_clears_on_release_with_a_persistent_slc_override(pedal_snapshot, monkeypatch):
|
|
||||||
widget, ui = pedal_snapshot
|
|
||||||
sm = ui.sm
|
|
||||||
state = {
|
|
||||||
"speed_conversion": CV.MS_TO_MPH, "accepted_speed_limit_ms": 65 * CV.MPH_TO_MS,
|
|
||||||
"effective_target_ms": 70 * CV.MPH_TO_MS, "offset_ms": 5 * CV.MPH_TO_MS,
|
|
||||||
"speed_limit_changed": False, "unconfirmed_valid": False, "presented_source": "Map Data",
|
|
||||||
"slc_is_limiting_max_set": False, "slc_overridden_speed": 80 * CV.MPH_TO_MS,
|
|
||||||
}
|
|
||||||
monkeypatch.setattr(unified_speed, "_get_slc_state", lambda: state)
|
|
||||||
widget._refresh_snapshot()
|
|
||||||
assert widget._pedal_override
|
|
||||||
presentation = widget._presentation
|
|
||||||
|
|
||||||
sm["carState"].gasPressed = False
|
|
||||||
widget._refresh_snapshot()
|
|
||||||
assert widget._pedal_override
|
|
||||||
sm.frame += 1
|
|
||||||
sm.recv_frame["carState"] = sm.frame
|
|
||||||
widget._refresh_snapshot()
|
|
||||||
assert not widget._pedal_override
|
|
||||||
assert widget._presentation == presentation
|
|
||||||
assert widget._slc_state["slc_overridden_speed"] > 0
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ["split", "merged", "max_only", "limit_only"])
|
|
||||||
@pytest.mark.parametrize("unit", ["mph", "km/h"])
|
|
||||||
def test_pedal_cue_mutes_targets_and_preserves_units_offsets_and_layout(monkeypatch, mode, unit):
|
|
||||||
widget = make_widget(mode)
|
|
||||||
widget._pedal_override = True
|
|
||||||
widget._font_semi_bold = None
|
|
||||||
widget._show_max = mode != "limit_only"
|
|
||||||
widget._presentation = replace(widget._presentation, unit_text=unit)
|
|
||||||
if mode in ("max_only", "limit_only"):
|
|
||||||
widget._rect.width = 250
|
|
||||||
values, headers, pauses, offsets, lines = [], [], [], [], []
|
|
||||||
monkeypatch.setattr(unified_speed, "ui_state", SimpleNamespace(status=unified_speed.UIStatus.ENGAGED))
|
|
||||||
monkeypatch.setattr(unified_speed, "draw_control_card", lambda *args, **kwargs: None)
|
|
||||||
monkeypatch.setattr(unified_speed, "measure_text_cached", lambda *args: rl.Vector2(60, 28))
|
|
||||||
monkeypatch.setattr(rl, "draw_rectangle_rounded_lines_ex", lambda *args: None)
|
|
||||||
monkeypatch.setattr(rl, "draw_rectangle_rec", lambda *args: pauses.append(args))
|
|
||||||
monkeypatch.setattr(rl, "draw_line_ex", lambda *args: lines.append(args))
|
|
||||||
monkeypatch.setattr(widget, "_draw_merged_separator", lambda *args: None)
|
|
||||||
monkeypatch.setattr(widget, "_draw_header", lambda bounds, text, icon, color: headers.append(color))
|
|
||||||
monkeypatch.setattr(widget, "_draw_centered_text", lambda text, bounds, y, size, color, **kwargs: values.append((text, bounds, size, color)))
|
|
||||||
monkeypatch.setattr(widget, "_draw_offset_pill", lambda bounds, text, y: offsets.append(text))
|
|
||||||
|
|
||||||
widget._render(widget.rect)
|
|
||||||
speed_values = [value for value in values if value[2] == unified_speed.VALUE_FONT_SIZE]
|
|
||||||
unit_values = [value for value in values if value[2] == unified_speed.UNIT_FONT_SIZE]
|
|
||||||
expected_speeds = {"split": ["70", "65"], "merged": ["70"], "max_only": ["70"], "limit_only": ["65"]}
|
|
||||||
assert [value[0] for value in speed_values] == expected_speeds[mode]
|
|
||||||
assert all(value[3] == unified_speed.COLORS.DISENGAGED for value in speed_values + unit_values)
|
|
||||||
assert all(color == unified_speed.COLORS.DISENGAGED for color in headers)
|
|
||||||
assert [value[0] for value in unit_values] == [unit] * (2 if mode == "split" else 1)
|
|
||||||
assert len(pauses) == 2 * len(unit_values)
|
|
||||||
assert all(color == unified_speed.OFFSET_COLOR for _bounds, color in pauses)
|
|
||||||
assert offsets == ([] if mode == "max_only" else ["+5"])
|
|
||||||
assert not any(line[2] == 3 for line in lines)
|
|
||||||
for index, value in enumerate(unit_values):
|
|
||||||
pause = pauses[index * 2][0]
|
|
||||||
assert pause.x == pytest.approx(value[1].x + (value[1].width - 60) / 2 - 20)
|
|
||||||
|
|
||||||
values.clear()
|
|
||||||
pauses.clear()
|
|
||||||
widget._pedal_override = False
|
|
||||||
widget._render(widget.rect)
|
|
||||||
assert not pauses
|
|
||||||
assert all(value[3] == unified_speed.COLORS.WHITE for value in values if value[2] == unified_speed.VALUE_FONT_SIZE)
|
|
||||||
assert all(value[3] == unified_speed.COLORS.WHITE_TRANSLUCENT for value in values if value[2] == unified_speed.UNIT_FONT_SIZE)
|
|
||||||
|
|
||||||
|
|
||||||
def test_split_merged_transitions_keep_the_same_footprint(monkeypatch):
|
|
||||||
widget = make_widget("split")
|
|
||||||
monkeypatch.setattr(widget, "_refresh_snapshot", lambda: None)
|
|
||||||
sizes = []
|
|
||||||
for mode in ("split", "merged", "split", "merged"):
|
|
||||||
widget._presentation = replace(widget._presentation, mode=mode)
|
|
||||||
sizes.append(widget.get_size())
|
|
||||||
assert sizes == [(520.0, 250.0)] * 4
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def slc_ui(monkeypatch):
|
|
||||||
class Params(dict):
|
|
||||||
def get_bool(self, key):
|
|
||||||
return bool(self.get(key))
|
|
||||||
|
|
||||||
def get(self, key, encoding=None):
|
|
||||||
return super().get(key)
|
|
||||||
|
|
||||||
class SubMaster(dict):
|
|
||||||
recv_frame = {"starpilotPlan": 10}
|
|
||||||
valid = {"starpilotCarState": True}
|
|
||||||
|
|
||||||
plan = custom.StarPilotPlan.new_message(
|
|
||||||
slcSpeedLimit=30 * CV.MPH_TO_MS, slcSpeedLimitOffset=0.0, slcSpeedLimitSource="Map Data",
|
|
||||||
slcOverriddenSpeed=0.0, slcMapSpeedLimit=30 * CV.MPH_TO_MS, slcMapboxSpeedLimit=0.0,
|
|
||||||
slcNextSpeedLimit=0.0, unconfirmedSlcSpeedLimit=0.0, speedLimitChanged=False,
|
|
||||||
)
|
|
||||||
sm = SubMaster(starpilotPlan=plan, starpilotCarState=SimpleNamespace(dashboardSpeedLimit=0.0))
|
|
||||||
sm.recv_frame = sm.recv_frame.copy()
|
|
||||||
params = Params(SpeedLimitController=True, ShowSpeedLimits=False)
|
|
||||||
ui = SimpleNamespace(
|
|
||||||
sm=sm, started_frame=10, is_metric=False, ui_params=params, starpilot_toggles={},
|
|
||||||
params_memory=SimpleNamespace(get_float=lambda _key: 0.0),
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(slc_speed_limit, "ui_state", ui)
|
|
||||||
monkeypatch.setattr(slc_speed_limit, "starpilot_state", SimpleNamespace(car_state=SimpleNamespace(hasDashSpeedLimits=True)))
|
|
||||||
monkeypatch.setattr(slc_speed_limit, "_tick_pulse", lambda *args: None)
|
|
||||||
return ui
|
|
||||||
|
|
||||||
|
|
||||||
def test_slc_state_extraction_respects_feature_and_display_toggles(slc_ui):
|
|
||||||
assert slc_speed_limit._is_slc_enabled()
|
|
||||||
assert slc_speed_limit._get_slc_state()["slc_enabled"]
|
|
||||||
slc_ui.starpilot_toggles["speed_limit_controller"] = False
|
|
||||||
assert not slc_speed_limit._is_slc_enabled()
|
|
||||||
assert slc_speed_limit._get_slc_state() is None
|
|
||||||
slc_ui.ui_params["ShowSpeedLimits"] = True
|
|
||||||
assert not slc_speed_limit._get_slc_state()["slc_enabled"]
|
|
||||||
slc_ui.starpilot_toggles["speed_limit_controller"] = True
|
|
||||||
slc_ui.sm.recv_frame["starpilotPlan"] = 9
|
|
||||||
assert slc_speed_limit._get_slc_state() is None
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("presented_source,expected_source,expected_speed", [
|
|
||||||
("", "Map Data", "30"),
|
|
||||||
("Map Data", "Map Data", "30"),
|
|
||||||
("None", "None", "–"),
|
|
||||||
("Previous Limit", "Previous Limit", "30"),
|
|
||||||
("Vision", "Vision", "30"),
|
|
||||||
])
|
|
||||||
def test_serialized_plan_source_defaults_and_explicit_values(slc_ui, presented_source, expected_source, expected_speed):
|
|
||||||
message = slc_ui.sm["starpilotPlan"]
|
|
||||||
if presented_source:
|
|
||||||
message.slcPresentedSpeedLimitSource = presented_source
|
|
||||||
# Replay decodes older plans with a present but empty Text attribute.
|
|
||||||
with custom.StarPilotPlan.from_bytes(message.to_bytes()) as plan:
|
|
||||||
slc_ui.sm["starpilotPlan"] = plan
|
|
||||||
state = slc_speed_limit._get_slc_state()
|
|
||||||
result = resolve_unified_speed(True, True, 35, state, True, False)
|
|
||||||
assert result.source == expected_source
|
|
||||||
assert result.posted_speed_text == expected_speed
|
|
||||||
assert result.mode == "split"
|
|
||||||
|
|
||||||
|
|
||||||
def test_legacy_replay_limit_and_offset_merge_with_max_set(slc_ui):
|
|
||||||
message = slc_ui.sm["starpilotPlan"]
|
|
||||||
message.slcSpeedLimitOffset = 5 * CV.MPH_TO_MS
|
|
||||||
with custom.StarPilotPlan.from_bytes(message.to_bytes()) as plan:
|
|
||||||
slc_ui.sm["starpilotPlan"] = plan
|
|
||||||
result = resolve_unified_speed(True, True, 35, slc_speed_limit._get_slc_state(), True, False)
|
|
||||||
assert (result.source, result.posted_speed_text, result.effective_speed_text) == ("Map Data", "30", "35")
|
|
||||||
assert (result.mode, result.offset_text) == ("merged", "+5")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("presented_source,limiting,max_speed,enabled,overridden,expected_side,line_x", [
|
|
||||||
("", False, 40, True, False, "slc", 308),
|
|
||||||
("", False, 34, True, False, "max", 48),
|
|
||||||
("", False, 35, True, False, "shared", None),
|
|
||||||
("", False, 40, False, False, "max", 48),
|
|
||||||
("", False, 40, True, True, "none", None),
|
|
||||||
("Map Data", False, 40, True, False, "max", 48),
|
|
||||||
("Map Data", True, 40, True, False, "slc", 308),
|
|
||||||
])
|
|
||||||
def test_active_underline_with_legacy_and_current_plans(slc_ui, monkeypatch, presented_source, limiting,
|
|
||||||
max_speed, enabled, overridden, expected_side, line_x):
|
|
||||||
message = slc_ui.sm["starpilotPlan"]
|
|
||||||
message.slcSpeedLimitOffset = 5 * CV.MPH_TO_MS
|
|
||||||
message.slcPresentedSpeedLimitSource = presented_source
|
|
||||||
message.slcIsLimitingMaxSet = limiting
|
|
||||||
message.slcOverriddenSpeed = 40 * CV.MPH_TO_MS if overridden else 0.0
|
|
||||||
slc_ui.starpilot_toggles["speed_limit_controller"] = enabled
|
|
||||||
slc_ui.ui_params["ShowSpeedLimits"] = True
|
|
||||||
with custom.StarPilotPlan.from_bytes(message.to_bytes()) as plan:
|
|
||||||
slc_ui.sm["starpilotPlan"] = plan
|
|
||||||
presentation = resolve_unified_speed(True, True, max_speed, slc_speed_limit._get_slc_state(), enabled, False)
|
|
||||||
assert presentation.active_side == expected_side
|
|
||||||
|
|
||||||
widget = make_widget(presentation.mode)
|
|
||||||
widget._presentation = presentation
|
|
||||||
monkeypatch.setattr(unified_speed, "ui_state", SimpleNamespace(status=unified_speed.UIStatus.ENGAGED))
|
|
||||||
lines = []
|
|
||||||
monkeypatch.setattr(unified_speed.rl, "draw_line_ex", lambda *args: lines.append(args))
|
|
||||||
widget._draw_active_emphasis(widget.rect)
|
|
||||||
if line_x is None:
|
|
||||||
assert lines == []
|
|
||||||
else:
|
|
||||||
assert len(lines) == 1
|
|
||||||
assert (lines[0][0].x, lines[0][0].y, lines[0][1].x) == (line_x, 140, line_x + 224)
|
|
||||||
assert lines[0][3] == unified_speed.UNIFIED_ACCENT
|
|
||||||
|
|
||||||
|
|
||||||
def test_legacy_plan_without_active_source_does_not_use_diagnostic_map_limit(slc_ui):
|
|
||||||
message = slc_ui.sm["starpilotPlan"]
|
|
||||||
message.slcSpeedLimitSource = "None"
|
|
||||||
with custom.StarPilotPlan.from_bytes(message.to_bytes()) as plan:
|
|
||||||
slc_ui.sm["starpilotPlan"] = plan
|
|
||||||
state = slc_speed_limit._get_slc_state()
|
|
||||||
assert round(state["map_sl"]) == 30
|
|
||||||
result = resolve_unified_speed(True, True, 35, state, True, False)
|
|
||||||
assert (result.source, result.posted_speed_text, result.mode) == ("None", "–", "split")
|
|
||||||
|
|
||||||
|
|
||||||
def test_legacy_pending_candidate_remains_visible_without_active_source(slc_ui):
|
|
||||||
message = slc_ui.sm["starpilotPlan"]
|
|
||||||
message.slcSpeedLimitSource = "None"
|
|
||||||
message.unconfirmedSlcSpeedLimit = 45 * CV.MPH_TO_MS
|
|
||||||
message.speedLimitChanged = True
|
|
||||||
with custom.StarPilotPlan.from_bytes(message.to_bytes()) as plan:
|
|
||||||
slc_ui.sm["starpilotPlan"] = plan
|
|
||||||
result = resolve_unified_speed(True, True, 35, slc_speed_limit._get_slc_state(), True, False)
|
|
||||||
assert (result.posted_speed_text, result.mode, result.confirmation_pending) == ("45", "split", True)
|
|
||||||
|
|
||||||
|
|
||||||
def test_header_colors_preserve_engaged_disengaged_and_override_semantics(monkeypatch):
|
|
||||||
widget = make_widget()
|
|
||||||
colors = unified_speed.COLORS
|
|
||||||
monkeypatch.setattr(unified_speed, "ui_state", SimpleNamespace(status=unified_speed.UIStatus.ENGAGED))
|
|
||||||
assert widget._max_header_color("max", True) == colors.ENGAGED
|
|
||||||
assert widget._max_header_color("slc", True) == colors.GREY
|
|
||||||
assert widget._limit_header_color("slc", False) == colors.ENGAGED
|
|
||||||
|
|
||||||
monkeypatch.setattr(unified_speed, "ui_state", SimpleNamespace(status=unified_speed.UIStatus.DISENGAGED))
|
|
||||||
assert widget._max_header_color("max", True) == colors.DISENGAGED
|
|
||||||
assert widget._limit_header_color("slc", False) == colors.DISENGAGED
|
|
||||||
|
|
||||||
monkeypatch.setattr(unified_speed, "ui_state", SimpleNamespace(status=unified_speed.UIStatus.OVERRIDE))
|
|
||||||
assert widget._max_header_color("max", True) == colors.DISENGAGED
|
|
||||||
assert widget._limit_header_color("slc", False) == colors.DISENGAGED
|
|
||||||
|
|
||||||
monkeypatch.setattr(unified_speed, "ui_state", SimpleNamespace(status=unified_speed.UIStatus.ENGAGED))
|
|
||||||
assert widget._limit_header_color("none", True) == colors.DISENGAGED
|
|
||||||
@@ -137,19 +137,6 @@ class TestWidgetLayoutManager(unittest.TestCase):
|
|||||||
# w3: should stack directly below w1: y = 75 + 100 + 15 = 190
|
# w3: should stack directly below w1: y = 75 + 100 + 15 = 190
|
||||||
self.assertEqual(w3.rect.y, 190)
|
self.assertEqual(w3.rect.y, 190)
|
||||||
|
|
||||||
def test_wide_unified_card_stays_inside_the_left_edge(self):
|
|
||||||
card = DummyLayoutWidget("unified_speed", priority=1, width=520, height=250)
|
|
||||||
gauge = DummyLayoutWidget("aethergauge", priority=3, width=176, height=260)
|
|
||||||
self.layout_manager.register_widget("left", card)
|
|
||||||
self.layout_manager.register_widget("left", gauge)
|
|
||||||
|
|
||||||
self.layout_manager.update_layout(self.content_rect)
|
|
||||||
|
|
||||||
self.assertEqual(card.rect.x, self.content_rect.x + 30)
|
|
||||||
self.assertEqual(card.rect.y, self.content_rect.y + 45)
|
|
||||||
self.assertEqual(gauge.rect.x + gauge.rect.width / 2, self.content_rect.x + 146)
|
|
||||||
self.assertEqual(gauge.rect.y, card.rect.y + card.rect.height + self.layout_manager.spacing)
|
|
||||||
|
|
||||||
def test_dynamic_repositioning_on_rect_change(self):
|
def test_dynamic_repositioning_on_rect_change(self):
|
||||||
# Register a widget
|
# Register a widget
|
||||||
w1 = DummyLayoutWidget("w1", priority=1, width=100, height=100)
|
w1 = DummyLayoutWidget("w1", priority=1, width=100, height=100)
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ MACH_E_TURN_IN_LOOKAHEAD_EXTRA = 0.80
|
|||||||
MACH_E_LOW_SPEED_TURN_IN_LOOKAHEAD_EXTRA = 1.60
|
MACH_E_LOW_SPEED_TURN_IN_LOOKAHEAD_EXTRA = 1.60
|
||||||
MACH_E_LOW_SPEED_TURN_IN_START_SPEED = 2.0
|
MACH_E_LOW_SPEED_TURN_IN_START_SPEED = 2.0
|
||||||
MACH_E_LOW_SPEED_TURN_IN_FULL_SPEED = 3.0
|
MACH_E_LOW_SPEED_TURN_IN_FULL_SPEED = 3.0
|
||||||
MACH_E_LOW_SPEED_TURN_IN_MAX_SPEED = 9.0
|
MACH_E_LOW_SPEED_TURN_IN_MAX_SPEED = 11.0
|
||||||
MACH_E_LOW_SPEED_TURN_IN_FADE_SPEED = 12.0
|
MACH_E_LOW_SPEED_TURN_IN_FADE_SPEED = 14.0
|
||||||
MACH_E_TURN_IN_MIN_CURVATURE = 0.002
|
MACH_E_TURN_IN_MIN_CURVATURE = 0.002
|
||||||
MACH_E_TURN_IN_FULL_CURVATURE = 0.008
|
MACH_E_TURN_IN_FULL_CURVATURE = 0.008
|
||||||
MACH_E_TURN_IN_LAG_CURVATURE = 0.006
|
MACH_E_TURN_IN_LAG_CURVATURE = 0.006
|
||||||
@@ -78,7 +78,7 @@ MACH_E_SHARP_DIRECTION_CHANGE_MIN_ACCEL = 1.8
|
|||||||
MACH_E_SHARP_DIRECTION_CHANGE_FULL_ACCEL = 2.2
|
MACH_E_SHARP_DIRECTION_CHANGE_FULL_ACCEL = 2.2
|
||||||
MACH_E_SHARP_DIRECTION_CHANGE_MIN_LAG_CURVATURE = -0.0005
|
MACH_E_SHARP_DIRECTION_CHANGE_MIN_LAG_CURVATURE = -0.0005
|
||||||
MACH_E_SHARP_DIRECTION_CHANGE_FULL_LAG_CURVATURE = 0.0008
|
MACH_E_SHARP_DIRECTION_CHANGE_FULL_LAG_CURVATURE = 0.0008
|
||||||
MACH_E_UNDERSTEER_ERROR_MAX = 0.006
|
MACH_E_CURVATURE_ERROR_MAX = 0.006
|
||||||
MACH_E_UNDERSTEER_ERROR_MIN_DEFICIT = 0.002
|
MACH_E_UNDERSTEER_ERROR_MIN_DEFICIT = 0.002
|
||||||
MACH_E_UNDERSTEER_ERROR_FULL_DEFICIT = 0.004
|
MACH_E_UNDERSTEER_ERROR_FULL_DEFICIT = 0.004
|
||||||
MACH_E_PATH_ANGLE_MAX = 0.16
|
MACH_E_PATH_ANGLE_MAX = 0.16
|
||||||
@@ -87,6 +87,9 @@ MACH_E_PATH_ANGLE_FADE_START_SPEED = 8.0
|
|||||||
MACH_E_PATH_ANGLE_MAX_SPEED = 8.8
|
MACH_E_PATH_ANGLE_MAX_SPEED = 8.8
|
||||||
MACH_E_PATH_ANGLE_TRACKING_FACTOR = 0.75
|
MACH_E_PATH_ANGLE_TRACKING_FACTOR = 0.75
|
||||||
MACH_E_PATH_ANGLE_DRIVER_COOLDOWN = 0.75
|
MACH_E_PATH_ANGLE_DRIVER_COOLDOWN = 0.75
|
||||||
|
MACH_E_DRIVER_ASSIST_MIN_SPEED = 2.0
|
||||||
|
MACH_E_DRIVER_ASSIST_MAX_SPEED = 15.0
|
||||||
|
MACH_E_DRIVER_ASSIST_MAX_TORQUE = 3.5
|
||||||
FORD_CURVATURE_LOOKAHEAD = {
|
FORD_CURVATURE_LOOKAHEAD = {
|
||||||
CAR.FORD_EXPLORER_MK6: 0.20,
|
CAR.FORD_EXPLORER_MK6: 0.20,
|
||||||
}
|
}
|
||||||
@@ -223,17 +226,42 @@ class FordLateralController:
|
|||||||
def _current_curvature(CS) -> float:
|
def _current_curvature(CS) -> float:
|
||||||
return -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1)
|
return -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1)
|
||||||
|
|
||||||
|
def _driver_assisting_curve(self, CS, desired: float) -> bool:
|
||||||
|
v_ego = float(CS.out.vEgoRaw)
|
||||||
|
if (self.CP.carFingerprint != CAR.FORD_MUSTANG_MACH_E_MK1 or not self.CP.flags & FordFlags.CANFD or
|
||||||
|
not CS.out.steeringPressed or not MACH_E_DRIVER_ASSIST_MIN_SPEED <= v_ego < MACH_E_DRIVER_ASSIST_MAX_SPEED or
|
||||||
|
self._lane_change()[0] or abs(desired) < MACH_E_TURN_IN_MIN_CURVATURE or
|
||||||
|
desired * CS.out.steeringTorque >= 0.0 or abs(CS.out.steeringTorque) > MACH_E_DRIVER_ASSIST_MAX_TORQUE):
|
||||||
|
return False
|
||||||
|
current = self._current_curvature(CS)
|
||||||
|
preview = self._predicted_curvature(v_ego, self._curvature_lookahead() + MACH_E_TURN_IN_LOOKAHEAD_EXTRA)
|
||||||
|
return bool(desired * current > 0.0 and desired * preview > 0.0 and
|
||||||
|
abs(preview) >= MACH_E_TURN_IN_FULL_CURVATURE and
|
||||||
|
np.sign(desired) * (current - desired) <= CarControllerParams.CURVATURE_ERROR)
|
||||||
|
|
||||||
def _curvature_error_limit(self, requested: float, desired: float, current: float, v_ego: float,
|
def _curvature_error_limit(self, requested: float, desired: float, current: float, v_ego: float,
|
||||||
steering_pressed: bool, lane_change: bool) -> float:
|
steering_pressed: bool, lane_change: bool, predicted: float = 0.0) -> float:
|
||||||
base = CarControllerParams.CURVATURE_ERROR
|
base = CarControllerParams.CURVATURE_ERROR
|
||||||
if (self.CP.carFingerprint != CAR.FORD_MUSTANG_MACH_E_MK1 or not self.CP.flags & FordFlags.CANFD or
|
if (self.CP.carFingerprint != CAR.FORD_MUSTANG_MACH_E_MK1 or not self.CP.flags & FordFlags.CANFD or
|
||||||
steering_pressed or lane_change or requested * desired <= 0.0 or abs(desired) < 0.003):
|
steering_pressed or lane_change):
|
||||||
return base
|
return base
|
||||||
deficit = np.sign(desired) * (requested - current)
|
|
||||||
speed_weight = float(np.interp(v_ego, [9.0, 10.0, 14.0, 16.0], [0.0, 1.0, 1.0, 0.0]))
|
speed_weight = float(np.interp(v_ego, [9.0, 10.0, 14.0, 16.0], [0.0, 1.0, 1.0, 0.0]))
|
||||||
deficit_weight = float(np.interp(
|
deficit_weight = 0.0
|
||||||
deficit, [MACH_E_UNDERSTEER_ERROR_MIN_DEFICIT, MACH_E_UNDERSTEER_ERROR_FULL_DEFICIT], [0.0, 1.0]))
|
planned_curve = abs(desired) >= 0.003 or (abs(requested) >= 0.003 and requested * predicted > 0.0)
|
||||||
return base + (MACH_E_UNDERSTEER_ERROR_MAX - base) * speed_weight * deficit_weight
|
if requested * desired > 0.0 and planned_curve:
|
||||||
|
deficit = np.sign(desired) * (requested - current)
|
||||||
|
deficit_weight = float(np.interp(
|
||||||
|
deficit, [MACH_E_UNDERSTEER_ERROR_MIN_DEFICIT, MACH_E_UNDERSTEER_ERROR_FULL_DEFICIT], [0.0, 1.0]))
|
||||||
|
if current * desired < 0.0:
|
||||||
|
predicted = self._predicted_curvature(v_ego, self._curvature_lookahead() + MACH_E_TURN_IN_LOOKAHEAD_EXTRA)
|
||||||
|
reversal_weight = 0.0
|
||||||
|
if current * predicted < 0.0 and current * (requested - current) < 0.0:
|
||||||
|
reversal_weight = float(np.interp(
|
||||||
|
abs(predicted),
|
||||||
|
[MACH_E_DIRECTION_CHANGE_MIN_PREVIEW_CURVATURE, MACH_E_DIRECTION_CHANGE_FULL_PREVIEW_CURVATURE],
|
||||||
|
[0.0, 1.0],
|
||||||
|
))
|
||||||
|
return base + (MACH_E_CURVATURE_ERROR_MAX - base) * speed_weight * max(deficit_weight, reversal_weight)
|
||||||
|
|
||||||
def _path_angle_assist(self, requested: float, desired: float, applied: float, current: float, v_ego: float,
|
def _path_angle_assist(self, requested: float, desired: float, applied: float, current: float, v_ego: float,
|
||||||
steering_pressed: bool, lane_change: bool) -> float:
|
steering_pressed: bool, lane_change: bool) -> float:
|
||||||
@@ -246,7 +274,7 @@ class FordLateralController:
|
|||||||
not steering_pressed and self.path_angle_driver_cooldown == 0.0 and not lane_change and
|
not steering_pressed and self.path_angle_driver_cooldown == 0.0 and not lane_change and
|
||||||
3.0 <= v_ego < MACH_E_PATH_ANGLE_MAX_SPEED and
|
3.0 <= v_ego < MACH_E_PATH_ANGLE_MAX_SPEED and
|
||||||
requested * desired > 0.0 and requested * applied > 0.0 and
|
requested * desired > 0.0 and requested * applied > 0.0 and
|
||||||
abs(requested) > 0.0198 and abs(desired) > 0.016 and abs(applied) >= 0.0195 and
|
abs(requested) > 0.0198 and abs(desired) > MACH_E_TURN_IN_FULL_CURVATURE and abs(applied) >= 0.0195 and
|
||||||
np.sign(desired) * (desired - current) > 0.002):
|
np.sign(desired) * (desired - current) > 0.002):
|
||||||
max_curvature = MAX_LATERAL_ACCEL / v_ego ** 2
|
max_curvature = MAX_LATERAL_ACCEL / v_ego ** 2
|
||||||
residual = max(0.0, min(max(abs(requested), abs(desired)), max_curvature) - abs(applied))
|
residual = max(0.0, min(max(abs(requested), abs(desired)), max_curvature) - abs(applied))
|
||||||
@@ -427,7 +455,7 @@ class FordLateralController:
|
|||||||
))
|
))
|
||||||
return speed_weight * curvature_weight * preview_weight * acceleration_weight
|
return speed_weight * curvature_weight * preview_weight * acceleration_weight
|
||||||
|
|
||||||
def _manual_turn(self, CC, CS, desired: float) -> bool:
|
def _manual_turn(self, CC, CS, desired: float, driver_assisting: bool = False) -> bool:
|
||||||
if not CC.latActive:
|
if not CC.latActive:
|
||||||
self.human_turn.reset()
|
self.human_turn.reset()
|
||||||
self.manual_turn_latched = False
|
self.manual_turn_latched = False
|
||||||
@@ -435,7 +463,7 @@ class FordLateralController:
|
|||||||
self.manual_turn_direction = 0.0
|
self.manual_turn_direction = 0.0
|
||||||
return False
|
return False
|
||||||
detected = self.human_turn.update(
|
detected = self.human_turn.update(
|
||||||
self.human_turn_enabled, CS.out.steeringPressed, CS.out.steeringAngleDeg)
|
self.human_turn_enabled and not driver_assisting, CS.out.steeringPressed, CS.out.steeringAngleDeg)
|
||||||
if self.CP.carFingerprint not in FORD_MANUAL_TURN_LATCH_CARS:
|
if self.CP.carFingerprint not in FORD_MANUAL_TURN_LATCH_CARS:
|
||||||
return detected
|
return detected
|
||||||
|
|
||||||
@@ -447,7 +475,7 @@ class FordLateralController:
|
|||||||
|
|
||||||
blinker_direction = float(CS.out.rightBlinker) - float(CS.out.leftBlinker)
|
blinker_direction = float(CS.out.rightBlinker) - float(CS.out.leftBlinker)
|
||||||
driver_turning_with_signal = (
|
driver_turning_with_signal = (
|
||||||
CS.out.steeringPressed and abs(CS.out.steeringAngleDeg) >= MANUAL_TURN_ENTRY_ANGLE_DEG and
|
CS.out.steeringPressed and not driver_assisting and abs(CS.out.steeringAngleDeg) >= MANUAL_TURN_ENTRY_ANGLE_DEG and
|
||||||
blinker_direction != 0.0 and not self._lane_change()[0] and
|
blinker_direction != 0.0 and not self._lane_change()[0] and
|
||||||
CS.out.steeringTorque * blinker_direction < 0.0
|
CS.out.steeringTorque * blinker_direction < 0.0
|
||||||
)
|
)
|
||||||
@@ -494,7 +522,10 @@ class FordLateralController:
|
|||||||
self.desired_curvature_last = 0.0
|
self.desired_curvature_last = 0.0
|
||||||
return FordLateralResult()
|
return FordLateralResult()
|
||||||
|
|
||||||
manual_turn = self._manual_turn(CC, CS, float(actuators.curvature))
|
desired = float(actuators.curvature)
|
||||||
|
driver_assisting = self._driver_assisting_curve(CS, desired)
|
||||||
|
driver_override = bool(CS.out.steeringPressed) and not driver_assisting
|
||||||
|
manual_turn = self._manual_turn(CC, CS, desired, driver_assisting)
|
||||||
if manual_turn or CS.out.vEgoRaw < 0.1:
|
if manual_turn or CS.out.vEgoRaw < 0.1:
|
||||||
if CS.out.steeringPressed:
|
if CS.out.steeringPressed:
|
||||||
self.path_angle_driver_cooldown = MACH_E_PATH_ANGLE_DRIVER_COOLDOWN
|
self.path_angle_driver_cooldown = MACH_E_PATH_ANGLE_DRIVER_COOLDOWN
|
||||||
@@ -508,7 +539,6 @@ class FordLateralController:
|
|||||||
v_ego = float(CS.out.vEgoRaw)
|
v_ego = float(CS.out.vEgoRaw)
|
||||||
lookahead = self._curvature_lookahead()
|
lookahead = self._curvature_lookahead()
|
||||||
predicted = self._predicted_curvature(v_ego, lookahead)
|
predicted = self._predicted_curvature(v_ego, lookahead)
|
||||||
desired = float(actuators.curvature)
|
|
||||||
allow_opposite_preview = False
|
allow_opposite_preview = False
|
||||||
if self.CP.carFingerprint in FORD_CONSERVATIVE_PREVIEW_CARS:
|
if self.CP.carFingerprint in FORD_CONSERVATIVE_PREVIEW_CARS:
|
||||||
turn_in_predicted = self._predicted_curvature(v_ego, lookahead + MACH_E_TURN_IN_LOOKAHEAD_EXTRA)
|
turn_in_predicted = self._predicted_curvature(v_ego, lookahead + MACH_E_TURN_IN_LOOKAHEAD_EXTRA)
|
||||||
@@ -565,7 +595,7 @@ class FordLateralController:
|
|||||||
|
|
||||||
if v_ego > 9.0:
|
if v_ego > 9.0:
|
||||||
error_limit = self._curvature_error_limit(
|
error_limit = self._curvature_error_limit(
|
||||||
requested, desired, current, v_ego, bool(CS.out.steeringPressed), self._lane_change()[0])
|
requested, desired, current, v_ego, driver_override, self._lane_change()[0], command_predicted)
|
||||||
requested = float(np.clip(requested, current - error_limit, current + error_limit))
|
requested = float(np.clip(requested, current - error_limit, current + error_limit))
|
||||||
applied = float(apply_std_steer_angle_limits(
|
applied = float(apply_std_steer_angle_limits(
|
||||||
requested, self.curvature_last, v_ego, CS.out.steeringAngleDeg, True, FORD_CURVATURE_LIMITS))
|
requested, self.curvature_last, v_ego, CS.out.steeringAngleDeg, True, FORD_CURVATURE_LIMITS))
|
||||||
@@ -573,7 +603,7 @@ class FordLateralController:
|
|||||||
max_curvature = MAX_LATERAL_ACCEL / max(v_ego, 1.0) ** 2
|
max_curvature = MAX_LATERAL_ACCEL / max(v_ego, 1.0) ** 2
|
||||||
applied = float(np.clip(applied, -max_curvature, max_curvature))
|
applied = float(np.clip(applied, -max_curvature, max_curvature))
|
||||||
path_angle = self._path_angle_assist(
|
path_angle = self._path_angle_assist(
|
||||||
requested, desired, applied, current, v_ego, bool(CS.out.steeringPressed), self._lane_change()[0])
|
requested, desired, applied, current, v_ego, driver_override, self._lane_change()[0])
|
||||||
|
|
||||||
self.curvature_samples.append(predicted)
|
self.curvature_samples.append(predicted)
|
||||||
curvature_rate = 0.0
|
curvature_rate = 0.0
|
||||||
|
|||||||
@@ -117,6 +117,124 @@ def test_understeer_error_preserves_other_fords(controller):
|
|||||||
assert controller._curvature_error_limit(0.012, 0.012, 0.004, 12.0, False, False) == 0.002
|
assert controller._curvature_error_limit(0.012, 0.012, 0.004, 12.0, False, False) == 0.002
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("sign", (-1, 1))
|
||||||
|
@pytest.mark.parametrize("speed,expected", ((9.0, 0.002), (9.5, 0.004), (10.0, 0.006),
|
||||||
|
(12.0, 0.006), (15.0, 0.004), (16.0, 0.002)))
|
||||||
|
def test_mach_e_planned_curve_error_uses_preview_request_before_action_builds(controller, sign, speed, expected):
|
||||||
|
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||||
|
controller.CP.flags = FordFlags.CANFD
|
||||||
|
assert controller._curvature_error_limit(
|
||||||
|
sign * 0.006, sign * 0.001, sign * 0.0005, speed, False, False, sign * 0.012) == pytest.approx(expected)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("requested,desired,preview", ((0.006, 0.001, -0.012),
|
||||||
|
(0.006, 0.001, 0.0),
|
||||||
|
(0.006, -0.001, 0.012),
|
||||||
|
(0.0029, 0.001, 0.012)))
|
||||||
|
def test_mach_e_planned_curve_error_requires_curve_size_and_direction_agreement(controller, requested, desired, preview):
|
||||||
|
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||||
|
controller.CP.flags = FordFlags.CANFD
|
||||||
|
assert controller._curvature_error_limit(requested, desired, 0.0005, 12.0, False, False, preview) == 0.002
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("fingerprint,flags,driver,lane_change", (
|
||||||
|
(CAR.FORD_MUSTANG_MACH_E_MK1, FordFlags.CANFD, True, False),
|
||||||
|
(CAR.FORD_MUSTANG_MACH_E_MK1, FordFlags.CANFD, False, True),
|
||||||
|
(CAR.FORD_MUSTANG_MACH_E_MK1, 0, False, False),
|
||||||
|
(CAR.FORD_EDGE_MK2, FordFlags.CANFD, False, False),
|
||||||
|
(CAR.FORD_EXPLORER_MK6, FordFlags.CANFD, False, False),
|
||||||
|
(CAR.FORD_F_150_MK14, FordFlags.CANFD, False, False),
|
||||||
|
))
|
||||||
|
def test_planned_curve_error_preserves_takeover_lane_changes_and_other_fords(controller, fingerprint, flags, driver, lane_change):
|
||||||
|
controller.CP.carFingerprint = fingerprint
|
||||||
|
controller.CP.flags = flags
|
||||||
|
assert controller._curvature_error_limit(0.006, 0.001, 0.0005, 12.0, driver, lane_change, 0.012) == 0.002
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("sign", (-1, 1))
|
||||||
|
def test_mach_e_medium_speed_turn_in_lead_is_not_clipped_by_small_action_curvature(controller, monkeypatch, sign):
|
||||||
|
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||||
|
controller.CP.flags = FordFlags.CANFD
|
||||||
|
controller.sm["liveDelay"].lateralDelay = 0.4
|
||||||
|
controller.desired_curvature_last = sign * 0.0003
|
||||||
|
controller.curvature_last = sign * 0.003
|
||||||
|
monkeypatch.setattr(controller, "_predicted_curvature",
|
||||||
|
lambda _v, t: sign * (0.0007 if t < 1.0 else 0.0053 if t < 1.5 else 0.011))
|
||||||
|
result = controller.update(SimpleNamespace(latActive=True), car_state(speed=11.8, curvature=sign * 0.00027),
|
||||||
|
SimpleNamespace(curvature=sign * 0.0004))
|
||||||
|
assert result.active
|
||||||
|
assert 0.004 < sign * result.curvature <= 0.00627
|
||||||
|
assert result.path_angle == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("sign", (-1, 1))
|
||||||
|
@pytest.mark.parametrize("speed,preview,expected", (
|
||||||
|
(9.0, -0.004, 0.002),
|
||||||
|
(9.5, -0.004, 0.004),
|
||||||
|
(10.0, -0.004, 0.006),
|
||||||
|
(12.0, -0.004, 0.006),
|
||||||
|
(14.0, -0.004, 0.006),
|
||||||
|
(15.0, -0.004, 0.004),
|
||||||
|
(16.0, -0.004, 0.002),
|
||||||
|
(12.0, -0.0005, 0.002),
|
||||||
|
(12.0, -0.00125, 0.004),
|
||||||
|
(12.0, 0.004, 0.002),
|
||||||
|
(12.0, 0.0, 0.002),
|
||||||
|
))
|
||||||
|
def test_mach_e_reversal_error_releases_measured_curvature_clamp(controller, sign, speed, preview, expected):
|
||||||
|
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||||
|
controller.CP.flags = FordFlags.CANFD
|
||||||
|
assert controller._curvature_error_limit(
|
||||||
|
sign * 0.001, sign * 0.002, sign * 0.005, speed, False, False, sign * preview) == pytest.approx(expected)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("fingerprint,flags,driver,lane_change", (
|
||||||
|
(CAR.FORD_MUSTANG_MACH_E_MK1, FordFlags.CANFD, True, False),
|
||||||
|
(CAR.FORD_MUSTANG_MACH_E_MK1, FordFlags.CANFD, False, True),
|
||||||
|
(CAR.FORD_MUSTANG_MACH_E_MK1, 0, False, False),
|
||||||
|
(CAR.FORD_EDGE_MK2, FordFlags.CANFD, False, False),
|
||||||
|
(CAR.FORD_EXPLORER_MK6, FordFlags.CANFD, False, False),
|
||||||
|
(CAR.FORD_F_150_MK14, FordFlags.CANFD, False, False),
|
||||||
|
))
|
||||||
|
def test_reversal_error_preserves_takeover_lane_changes_and_other_fords(controller, fingerprint, flags, driver, lane_change):
|
||||||
|
controller.CP.carFingerprint = fingerprint
|
||||||
|
controller.CP.flags = flags
|
||||||
|
assert controller._curvature_error_limit(0.001, 0.002, 0.005, 12.0, driver, lane_change, -0.004) == 0.002
|
||||||
|
|
||||||
|
|
||||||
|
def test_mach_e_reversal_error_does_not_increase_old_direction_command(controller):
|
||||||
|
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||||
|
controller.CP.flags = FordFlags.CANFD
|
||||||
|
assert controller._curvature_error_limit(0.008, 0.002, 0.005, 12.0, False, False, -0.004) == 0.002
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("sign", (-1, 1))
|
||||||
|
@pytest.mark.parametrize("preview,expected", ((-0.004, 0.006), (-0.0005, 0.002), (0.004, 0.002)))
|
||||||
|
def test_mach_e_reversal_error_requires_preview_agreement_after_desired_crosses_zero(
|
||||||
|
controller, monkeypatch, sign, preview, expected):
|
||||||
|
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||||
|
controller.CP.flags = FordFlags.CANFD
|
||||||
|
monkeypatch.setattr(controller, "_predicted_curvature", lambda *_: sign * preview)
|
||||||
|
assert controller._curvature_error_limit(
|
||||||
|
-sign * 0.00034, -sign * 0.00034, sign * 0.00462, 12.0, False, False, sign * 0.0007) == pytest.approx(expected)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("sign", (-1, 1))
|
||||||
|
def test_mach_e_reversal_does_not_reapply_old_direction_at_desired_zero_crossing(controller, monkeypatch, sign):
|
||||||
|
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||||
|
controller.CP.flags = FordFlags.CANFD
|
||||||
|
controller.sm["liveDelay"].lateralDelay = 0.4
|
||||||
|
controller.desired_curvature_last = sign * 0.00155
|
||||||
|
controller.curvature_last = -sign * 0.00065
|
||||||
|
monkeypatch.setattr(controller, "_predicted_curvature",
|
||||||
|
lambda _v, t: sign * 0.0007 if t < 1.0 else -sign * 0.005)
|
||||||
|
result = controller.update(SimpleNamespace(latActive=True), car_state(speed=12.0, curvature=sign * 0.00462),
|
||||||
|
SimpleNamespace(curvature=-sign * 0.00034))
|
||||||
|
assert result.active
|
||||||
|
assert result.curvature == pytest.approx(-sign * 0.00034)
|
||||||
|
assert result.path_angle == 0.0
|
||||||
|
|
||||||
|
|
||||||
def test_mach_e_path_angle_assist_at_curvature_limit(controller):
|
def test_mach_e_path_angle_assist_at_curvature_limit(controller):
|
||||||
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||||
controller.CP.flags = FordFlags.CANFD
|
controller.CP.flags = FordFlags.CANFD
|
||||||
@@ -144,7 +262,7 @@ def test_mach_e_path_angle_assist_starts_at_saturation_and_releases_after_driver
|
|||||||
@pytest.mark.parametrize("speed,requested,desired,applied,driver,lane_change", (
|
@pytest.mark.parametrize("speed,requested,desired,applied,driver,lane_change", (
|
||||||
(9.0, 0.04, 0.04, 0.02, False, False),
|
(9.0, 0.04, 0.04, 0.02, False, False),
|
||||||
(7.5, 0.0197, 0.04, 0.02, False, False),
|
(7.5, 0.0197, 0.04, 0.02, False, False),
|
||||||
(7.5, 0.04, 0.015, 0.02, False, False),
|
(7.5, 0.04, 0.007, 0.02, False, False),
|
||||||
(7.5, 0.04, 0.04, 0.018, False, False),
|
(7.5, 0.04, 0.04, 0.018, False, False),
|
||||||
(7.5, 0.04, 0.04, 0.02, True, False),
|
(7.5, 0.04, 0.04, 0.02, True, False),
|
||||||
(7.5, 0.04, 0.04, 0.02, False, True),
|
(7.5, 0.04, 0.04, 0.02, False, True),
|
||||||
@@ -171,6 +289,135 @@ def test_mach_e_path_angle_assist_is_encoded_with_curvature(controller):
|
|||||||
assert encoded_angle == pytest.approx(-assist)
|
assert encoded_angle == pytest.approx(-assist)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("sign", (-1, 1))
|
||||||
|
@pytest.mark.parametrize("speed,expected", ((1.9, False), (2.0, True), (3.0, True), (7.0, True),
|
||||||
|
(11.0, True), (14.9, True), (15.0, False)))
|
||||||
|
def test_mach_e_driver_curve_assistance_scope(controller, monkeypatch, sign, speed, expected):
|
||||||
|
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||||
|
controller.CP.flags = FordFlags.CANFD
|
||||||
|
monkeypatch.setattr(controller, "_predicted_curvature", lambda *_: sign * 0.02)
|
||||||
|
state = car_state(speed=speed, curvature=sign * 0.003, steering_pressed=True,
|
||||||
|
steering_torque=-sign * 2.0)
|
||||||
|
assert controller._driver_assisting_curve(state, sign * 0.004) is expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("driver,torque,current,desired,preview,lane_change", (
|
||||||
|
(False, -2.0, 0.008, 0.020, 0.025, False),
|
||||||
|
(True, 2.0, 0.008, 0.020, 0.025, False),
|
||||||
|
(True, -3.6, 0.008, 0.020, 0.025, False),
|
||||||
|
(True, -2.0, -0.008, 0.020, 0.025, False),
|
||||||
|
(True, -2.0, 0.023, 0.020, 0.025, False),
|
||||||
|
(True, -2.0, 0.001, 0.0019, 0.025, False),
|
||||||
|
(True, -2.0, 0.008, 0.020, -0.025, False),
|
||||||
|
(True, -2.0, 0.008, 0.020, 0.007, False),
|
||||||
|
(True, -2.0, 0.008, 0.020, 0.025, True),
|
||||||
|
))
|
||||||
|
def test_mach_e_driver_curve_assistance_requires_path_agreement(
|
||||||
|
controller, monkeypatch, driver, torque, current, desired, preview, lane_change):
|
||||||
|
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||||
|
controller.CP.flags = FordFlags.CANFD
|
||||||
|
monkeypatch.setattr(controller, "_predicted_curvature", lambda *_: preview)
|
||||||
|
monkeypatch.setattr(controller, "_lane_change", lambda: (lane_change, 0))
|
||||||
|
state = car_state(speed=7.0, curvature=current, steering_pressed=driver, steering_torque=torque)
|
||||||
|
assert not controller._driver_assisting_curve(state, desired)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("fingerprint,flags", ((CAR.FORD_EDGE_MK2, FordFlags.CANFD),
|
||||||
|
(CAR.FORD_F_150_MK14, FordFlags.CANFD),
|
||||||
|
(CAR.FORD_MUSTANG_MACH_E_MK1, 0)))
|
||||||
|
def test_driver_curve_assistance_preserves_other_fords(controller, monkeypatch, fingerprint, flags):
|
||||||
|
controller.CP.carFingerprint = fingerprint
|
||||||
|
controller.CP.flags = flags
|
||||||
|
monkeypatch.setattr(controller, "_predicted_curvature", lambda *_: 0.025)
|
||||||
|
state = car_state(speed=7.0, curvature=0.008, steering_pressed=True, steering_torque=-2.0)
|
||||||
|
assert not controller._driver_assisting_curve(state, 0.020)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("sign", (-1, 1))
|
||||||
|
def test_mach_e_driver_assistance_handoff_and_takeover(controller, monkeypatch, sign):
|
||||||
|
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||||
|
controller.CP.flags = FordFlags.CANFD
|
||||||
|
controller.curvature_last = sign * 0.020
|
||||||
|
monkeypatch.setattr(controller, "_predicted_curvature", lambda *_: sign * 0.030)
|
||||||
|
CC = SimpleNamespace(latActive=True)
|
||||||
|
actuators = SimpleNamespace(curvature=sign * 0.022)
|
||||||
|
helping = car_state(speed=7.0, curvature=sign * 0.008, steering_pressed=True,
|
||||||
|
steering_angle=-sign * 50.0, steering_torque=-sign * 2.0,
|
||||||
|
left_blinker=sign < 0, right_blinker=sign > 0)
|
||||||
|
for _ in range(round(3.5 / STEER_DT)):
|
||||||
|
result = controller.update(CC, helping, actuators)
|
||||||
|
assert result.active
|
||||||
|
assert result.curvature == pytest.approx(sign * 0.020)
|
||||||
|
assert sign * result.path_angle > 0.0
|
||||||
|
assert not controller.manual_turn_latched
|
||||||
|
|
||||||
|
helping.out.steeringPressed = False
|
||||||
|
helping.out.steeringTorque = 0.0
|
||||||
|
result = controller.update(CC, helping, actuators)
|
||||||
|
assert result.active
|
||||||
|
assert sign * result.path_angle > 0.0
|
||||||
|
assert controller.path_angle_driver_cooldown == 0.0
|
||||||
|
|
||||||
|
helping.out.steeringPressed = True
|
||||||
|
helping.out.steeringTorque = sign * 2.0
|
||||||
|
result = controller.update(CC, helping, actuators)
|
||||||
|
assert result.path_angle == 0.0
|
||||||
|
assert controller.path_angle_driver_cooldown > 0.0
|
||||||
|
|
||||||
|
helping.out.steeringTorque = -sign * 3.6
|
||||||
|
result = controller.update(CC, helping, actuators)
|
||||||
|
assert not result.active
|
||||||
|
assert result.curvature == result.path_angle == 0.0
|
||||||
|
assert controller.manual_turn_latched
|
||||||
|
helping.out.steeringTorque = -sign * 2.0
|
||||||
|
assert not controller.update(CC, helping, actuators).active
|
||||||
|
|
||||||
|
controller.update(SimpleNamespace(latActive=False), helping, actuators)
|
||||||
|
helping.out.steeringTorque = -sign * 2.0
|
||||||
|
helping.out.yawRate = -sign * 0.025 * helping.out.vEgoRaw
|
||||||
|
result = controller.update(CC, helping, actuators)
|
||||||
|
assert not result.active
|
||||||
|
assert result.curvature == result.path_angle == 0.0
|
||||||
|
assert controller.manual_turn_latched
|
||||||
|
|
||||||
|
controller.update(SimpleNamespace(latActive=False), helping, actuators)
|
||||||
|
assert not controller.manual_turn_latched
|
||||||
|
assert controller.path_angle_driver_cooldown == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("sign", (-1, 1))
|
||||||
|
def test_mach_e_driver_help_at_early_curve_entry(controller, monkeypatch, sign):
|
||||||
|
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||||
|
controller.CP.flags = FordFlags.CANFD
|
||||||
|
monkeypatch.setattr(controller, "_predicted_curvature", lambda *_: sign * 0.030)
|
||||||
|
state = car_state(speed=2.7, curvature=sign * 0.003, steering_pressed=True,
|
||||||
|
steering_torque=-sign * 2.0, steering_angle=-sign * 15.0,
|
||||||
|
left_blinker=sign < 0, right_blinker=sign > 0)
|
||||||
|
CC = SimpleNamespace(latActive=True)
|
||||||
|
assert controller.update(CC, state, SimpleNamespace(curvature=sign * 0.004)).active
|
||||||
|
state.out.vEgoRaw = 3.5
|
||||||
|
state.out.yawRate = -sign * 0.004 * state.out.vEgoRaw
|
||||||
|
for _ in range(8):
|
||||||
|
result = controller.update(CC, state, SimpleNamespace(curvature=sign * 0.022))
|
||||||
|
assert result.active
|
||||||
|
assert result.curvature == pytest.approx(sign * 0.020)
|
||||||
|
assert sign * result.path_angle > 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("sign", (-1, 1))
|
||||||
|
def test_mach_e_driver_help_preserves_curvature_error_authority(controller, monkeypatch, sign):
|
||||||
|
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||||
|
controller.CP.flags = FordFlags.CANFD
|
||||||
|
controller.curvature_last = sign * 0.010
|
||||||
|
monkeypatch.setattr(controller, "_predicted_curvature", lambda *_: sign * 0.018)
|
||||||
|
state = car_state(speed=11.0, curvature=sign * 0.006, steering_pressed=True,
|
||||||
|
steering_torque=-sign * 2.0, steering_angle=-sign * 20.0)
|
||||||
|
result = controller.update(SimpleNamespace(latActive=True), state, SimpleNamespace(curvature=sign * 0.012))
|
||||||
|
assert result.active
|
||||||
|
assert sign * result.curvature > 0.010
|
||||||
|
assert result.path_angle == 0.0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("sign", (-1, 1))
|
@pytest.mark.parametrize("sign", (-1, 1))
|
||||||
def test_mach_e_unwind_anticipates_opening_curve_before_current_request_is_met(controller, monkeypatch, sign):
|
def test_mach_e_unwind_anticipates_opening_curve_before_current_request_is_met(controller, monkeypatch, sign):
|
||||||
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
controller.CP.carFingerprint = CAR.FORD_MUSTANG_MACH_E_MK1
|
||||||
@@ -386,8 +633,11 @@ def test_mach_e_turn_in_preview_is_not_carried_into_unwind(controller):
|
|||||||
(3.0, 1.60),
|
(3.0, 1.60),
|
||||||
(8.0, 1.60),
|
(8.0, 1.60),
|
||||||
(9.0, 1.60),
|
(9.0, 1.60),
|
||||||
(10.5, 1.20),
|
(10.5, 1.60),
|
||||||
(12.0, 0.80),
|
(11.0, 1.60),
|
||||||
|
(12.0, 4.0 / 3.0),
|
||||||
|
(13.0, 16.0 / 15.0),
|
||||||
|
(14.0, 0.80),
|
||||||
(15.0, 0.80),
|
(15.0, 0.80),
|
||||||
))
|
))
|
||||||
def test_mach_e_turn_in_lookahead_extra_fades_by_speed(controller, speed, expected):
|
def test_mach_e_turn_in_lookahead_extra_fades_by_speed(controller, speed, expected):
|
||||||
@@ -749,7 +999,7 @@ def test_mach_e_extended_direction_horizon_does_not_replace_turn_in_preview(cont
|
|||||||
blend_inputs = []
|
blend_inputs = []
|
||||||
|
|
||||||
def predicted_curvature(_v_ego, lookahead):
|
def predicted_curvature(_v_ego, lookahead):
|
||||||
return {0.4: 0.006, 1.2: 0.010, 1.6: 0.004, 2.8: -0.002}[round(lookahead, 1)]
|
return {0.4: 0.006, 1.2: 0.010, 2.0: 0.004, 2.8: -0.002}[round(lookahead, 1)]
|
||||||
|
|
||||||
monkeypatch.setattr(controller, "_predicted_curvature", predicted_curvature)
|
monkeypatch.setattr(controller, "_predicted_curvature", predicted_curvature)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
|
|||||||
@@ -2370,8 +2370,8 @@
|
|||||||
{
|
{
|
||||||
"key": "ShowSLCOffset",
|
"key": "ShowSLCOffset",
|
||||||
"label": "Show Speed Limit Offset",
|
"label": "Show Speed Limit Offset",
|
||||||
"description": "Show the current offset on the compact driving display. The unified Max Set / Speed Limit card always shows nonzero offsets.",
|
"description": "Show the current offset from the posted limit on the driving screen.",
|
||||||
"picker_description": "Shows the offset on the compact display; the unified card always shows nonzero offsets.",
|
"picker_description": "Shows the current offset from the posted limit.",
|
||||||
"data_type": "bool",
|
"data_type": "bool",
|
||||||
"ui_type": "toggle",
|
"ui_type": "toggle",
|
||||||
"parent_key": "SpeedLimitController",
|
"parent_key": "SpeedLimitController",
|
||||||
@@ -2990,8 +2990,8 @@
|
|||||||
{
|
{
|
||||||
"key": "UseVienna",
|
"key": "UseVienna",
|
||||||
"label": "Use Vienna-Style Speed Signs",
|
"label": "Use Vienna-Style Speed Signs",
|
||||||
"description": "Use Vienna-style (EU) speed-limit signs on the compact driving display. The unified Max Set / Speed Limit card uses its own layout.",
|
"description": "Show Vienna-style (EU) speed-limit signs instead of MUTCD (US).",
|
||||||
"picker_description": "Uses Vienna-style signs on the compact display.",
|
"picker_description": "Uses Vienna-style speed-limit signs.",
|
||||||
"data_type": "bool",
|
"data_type": "bool",
|
||||||
"ui_type": "toggle",
|
"ui_type": "toggle",
|
||||||
"parent_key": "NavigationUI",
|
"parent_key": "NavigationUI",
|
||||||
@@ -4981,6 +4981,18 @@
|
|||||||
"is_parent_toggle": true,
|
"is_parent_toggle": true,
|
||||||
"settings_tier": "simple"
|
"settings_tier": "simple"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key": "StingerObjectShadow",
|
||||||
|
"label": "Stinger Object Logging Beta",
|
||||||
|
"description": "Collect candidate object data on the 2022-2023 Kia Stinger for validation. Logging only: this does not enable radar-based braking or change steering. Default off. Keep driving normally, stay attentive, and upload full rlogs with bookmarked observations.",
|
||||||
|
"picker_description": "Read-only Stinger object validation; no braking or steering changes.",
|
||||||
|
"data_type": "bool",
|
||||||
|
"ui_type": "toggle",
|
||||||
|
"galaxy_only": true,
|
||||||
|
"vehicle_makes": ["Kia"],
|
||||||
|
"parent_key": "GalaxyDeveloperMode",
|
||||||
|
"settings_tier": "advanced"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "ClusterOffset",
|
"key": "ClusterOffset",
|
||||||
"label": "Dashboard Speed Offset",
|
"label": "Dashboard Speed Offset",
|
||||||
|
|||||||
@@ -1461,8 +1461,6 @@ class StarPilotVariables:
|
|||||||
speed_limit_confirmation = self.get_value("SLCConfirmation", condition=toggle.speed_limit_controller)
|
speed_limit_confirmation = self.get_value("SLCConfirmation", condition=toggle.speed_limit_controller)
|
||||||
toggle.speed_limit_confirmation_higher = self.get_value("SLCConfirmationHigher", condition=speed_limit_confirmation)
|
toggle.speed_limit_confirmation_higher = self.get_value("SLCConfirmationHigher", condition=speed_limit_confirmation)
|
||||||
toggle.speed_limit_confirmation_lower = self.get_value("SLCConfirmationLower", condition=speed_limit_confirmation)
|
toggle.speed_limit_confirmation_lower = self.get_value("SLCConfirmationLower", condition=speed_limit_confirmation)
|
||||||
# Legacy setting is hidden in the current UI. SLC's pedal and +/- overrides
|
|
||||||
# remain available regardless of its saved value; keep loading it for compatibility.
|
|
||||||
slc_override_method = self.get_value("SLCOverride", cast=float, condition=toggle.speed_limit_controller)
|
slc_override_method = self.get_value("SLCOverride", cast=float, condition=toggle.speed_limit_controller)
|
||||||
toggle.speed_limit_controller_override_manual = slc_override_method == 1
|
toggle.speed_limit_controller_override_manual = slc_override_method == 1
|
||||||
toggle.speed_limit_controller_override_set_speed = slc_override_method == 2
|
toggle.speed_limit_controller_override_set_speed = slc_override_method == 2
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
import calendar
|
|
||||||
import json
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from openpilot.common.constants import CV
|
|
||||||
from openpilot.common.realtime import DT_MDL
|
|
||||||
from openpilot.starpilot.common.starpilot_utilities import calculate_bearing_offset, is_url_pingable
|
|
||||||
|
|
||||||
|
|
||||||
FREE_MAPBOX_REQUESTS = 100_000
|
|
||||||
|
|
||||||
|
|
||||||
class MapboxSpeedLimit:
|
|
||||||
def __init__(self, params):
|
|
||||||
self.params = params
|
|
||||||
try:
|
|
||||||
self.requests = json.loads(params.get("MapBoxRequests", encoding="utf-8") or "{}")
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
self.requests = {}
|
|
||||||
self.requests.setdefault("total_requests", 0)
|
|
||||||
self.requests.setdefault("max_requests", FREE_MAPBOX_REQUESTS - 28 * 100)
|
|
||||||
|
|
||||||
self.host = "https://api.mapbox.com"
|
|
||||||
self.token = params.get("MapboxSecretKey", encoding="utf-8")
|
|
||||||
self.limit = 0.0
|
|
||||||
self.segment_distance = 0.0
|
|
||||||
self.future = None
|
|
||||||
self.executor = ThreadPoolExecutor(max_workers=1)
|
|
||||||
self.session = requests.Session()
|
|
||||||
self.session.headers.update({"Accept-Language": "en"})
|
|
||||||
self.session.headers.update({"User-Agent": "starpilot-mapbox-speed-limit-retriever/1.0 (https://github.com/FrogAi/StarPilot)"})
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
# A discarded future may still finish, but only update() can publish its result.
|
|
||||||
if self.future is not None:
|
|
||||||
self.future.cancel()
|
|
||||||
self.future = None
|
|
||||||
self.limit = 0.0
|
|
||||||
self.segment_distance = 0.0
|
|
||||||
|
|
||||||
def shutdown(self):
|
|
||||||
self.reset()
|
|
||||||
self.executor.shutdown(wait=False, cancel_futures=True)
|
|
||||||
self.session.close()
|
|
||||||
|
|
||||||
def _request(self, position, v_ego):
|
|
||||||
if not is_url_pingable(self.host):
|
|
||||||
return 0.0, v_ego
|
|
||||||
|
|
||||||
self.requests["total_requests"] += 1
|
|
||||||
self.params.put_nonblocking("MapBoxRequests", json.dumps(self.requests))
|
|
||||||
|
|
||||||
bearing = position.get("bearing")
|
|
||||||
latitude = position.get("latitude")
|
|
||||||
longitude = position.get("longitude")
|
|
||||||
future_latitude, future_longitude = calculate_bearing_offset(latitude, longitude, bearing, v_ego)
|
|
||||||
url = f"{self.host}/matching/v5/mapbox/driving/{longitude},{latitude};{future_longitude},{future_latitude}.json"
|
|
||||||
params = {
|
|
||||||
"access_token": self.token,
|
|
||||||
"annotations": "maxspeed,distance",
|
|
||||||
"geometries": "polyline6",
|
|
||||||
"overview": "full",
|
|
||||||
"steps": "false",
|
|
||||||
"radiuses": "10;10",
|
|
||||||
"tidy": "true",
|
|
||||||
}
|
|
||||||
response = self.session.get(url, params=params, timeout=10)
|
|
||||||
response.raise_for_status()
|
|
||||||
matchings = response.json().get("matchings") or []
|
|
||||||
if not matchings:
|
|
||||||
return 0.0, v_ego
|
|
||||||
legs = (matchings[0] or {}).get("legs") or []
|
|
||||||
if not legs:
|
|
||||||
return 0.0, v_ego
|
|
||||||
|
|
||||||
annotation = legs[0].get("annotation") or {}
|
|
||||||
distances = annotation.get("distance") or [v_ego]
|
|
||||||
speeds = annotation.get("maxspeed") or []
|
|
||||||
if not speeds:
|
|
||||||
return 0.0, v_ego
|
|
||||||
first = speeds[0]
|
|
||||||
try:
|
|
||||||
speed = float(first.get("speed")) if first.get("speed") != "none" else 0.0
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
speed = 0.0
|
|
||||||
if speed <= 0:
|
|
||||||
return 0.0, v_ego
|
|
||||||
conversion = CV.MPH_TO_MS if first.get("unit", "km/h") == "mph" else CV.KPH_TO_MS
|
|
||||||
return speed * conversion, distances[0]
|
|
||||||
|
|
||||||
def update(self, now, time_validated, v_ego, gps_valid, position, steering_angle, angle_offset):
|
|
||||||
if not gps_valid or not self.token or abs(steering_angle - angle_offset) >= 45:
|
|
||||||
self.reset()
|
|
||||||
return
|
|
||||||
|
|
||||||
if time_validated and now.month != self.requests.get("month"):
|
|
||||||
self.requests.update({
|
|
||||||
"month": now.month,
|
|
||||||
"total_requests": 0,
|
|
||||||
"max_requests": FREE_MAPBOX_REQUESTS - calendar.monthrange(now.year, now.month)[1] * 100,
|
|
||||||
})
|
|
||||||
if self.requests["total_requests"] >= self.requests["max_requests"]:
|
|
||||||
self.reset()
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.future is not None:
|
|
||||||
if not self.future.done():
|
|
||||||
return
|
|
||||||
future = self.future
|
|
||||||
self.future = None
|
|
||||||
try:
|
|
||||||
self.limit, self.segment_distance = future.result()
|
|
||||||
except Exception as exception:
|
|
||||||
print(f"Unexpected error in Mapbox request: {exception}")
|
|
||||||
self.limit, self.segment_distance = 0.0, v_ego
|
|
||||||
return
|
|
||||||
|
|
||||||
if v_ego < 1:
|
|
||||||
return
|
|
||||||
if self.segment_distance > 0:
|
|
||||||
self.segment_distance -= v_ego * DT_MDL
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.future = self.executor.submit(self._request, dict(position), v_ego)
|
|
||||||
except RuntimeError:
|
|
||||||
self.segment_distance = v_ego
|
|
||||||
return
|
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# PFEIFER - SLC - Modified by FrogAi
|
# PFEIFER - SLC - Modified by FrogAi
|
||||||
|
import calendar
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
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.selfdrive.car.cruise import V_CRUISE_UNSET
|
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
|
||||||
|
|
||||||
from cereal import custom
|
from cereal import custom
|
||||||
from openpilot.starpilot.controls.lib.mapbox_speed_limit import MapboxSpeedLimit
|
from openpilot.starpilot.common.starpilot_utilities import calculate_bearing_offset, calculate_distance_to_point, is_url_pingable
|
||||||
|
|
||||||
|
FREE_MAPBOX_REQUESTS = 100_000
|
||||||
SOURCE_NONE = "None"
|
|
||||||
SOURCE_DASHBOARD = "Dashboard"
|
|
||||||
SOURCE_MAP = "Map Data"
|
|
||||||
SOURCE_VISION = "Vision"
|
|
||||||
SOURCE_MAPBOX = "Mapbox"
|
|
||||||
SOURCE_PREVIOUS_LIMIT = "Previous Limit"
|
|
||||||
REAL_SOURCES = (SOURCE_DASHBOARD, SOURCE_MAP, SOURCE_VISION, SOURCE_MAPBOX)
|
|
||||||
|
|
||||||
OFFSET_MAP_IMPERIAL = [
|
OFFSET_MAP_IMPERIAL = [
|
||||||
(0, 11.2, "speed_limit_offset1"), # 0–24 mph
|
(0, 11.2, "speed_limit_offset1"), # 0–24 mph
|
||||||
@@ -37,90 +36,83 @@ OFFSET_MAP_METRIC = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
SLC_OVERRIDE_DISABLE_CLEAR_TIME = 0.75
|
SLC_OVERRIDE_DISABLE_CLEAR_TIME = 0.75
|
||||||
SET_SPEED_CHANGE_TOLERANCE_METERS_PER_SECOND = 0.1
|
# Minimum set-speed increase (m/s) counted as a deliberate +/- press. Below the smallest
|
||||||
SAME_LIMIT_TOLERANCE = 1.0
|
# real step (1 km/h ≈ 0.28 m/s), above cluster/float jitter.
|
||||||
|
SET_SPEED_RAISE_EPS = 0.1
|
||||||
VISION_LARGE_REFERENCE_SPEED_DELTA = 30 * CV.MPH_TO_MS
|
VISION_LARGE_REFERENCE_SPEED_DELTA = 30 * CV.MPH_TO_MS
|
||||||
VISION_LARGE_SET_SPEED_MIN_SUPPORT = 3
|
VISION_LARGE_SET_SPEED_MIN_SUPPORT = 3
|
||||||
VISION_SUPPORT_SPEED_TOLERANCE = 0.5 * CV.MPH_TO_MS
|
VISION_SUPPORT_SPEED_TOLERANCE = 0.5 * CV.MPH_TO_MS
|
||||||
|
|
||||||
|
|
||||||
class SpeedLimitController:
|
class SpeedLimitController:
|
||||||
def __init__(self, StarPilotVCruise):
|
def __init__(self, StarPilotVCruise):
|
||||||
self.starpilot_planner = StarPilotVCruise.starpilot_planner
|
self.starpilot_planner = StarPilotVCruise.starpilot_planner
|
||||||
self.starpilot_toggles = None
|
self.starpilot_toggles = None
|
||||||
self.mapbox = MapboxSpeedLimit(self.starpilot_planner.params)
|
|
||||||
|
|
||||||
self.source = SOURCE_NONE
|
self.calling_mapbox = False
|
||||||
self.target = 0.0
|
self.override_slc = False
|
||||||
self.map_speed_limit = 0.0
|
self.override_disable_timer = 0.0
|
||||||
self.next_speed_limit = 0.0
|
self._prev_v_cruise = None
|
||||||
self.vision_limit = 0.0
|
self._persistent_override_speed = 0.0
|
||||||
self.overridden_speed = 0.0
|
self._set_speed_override_input_consumed = False
|
||||||
|
|
||||||
self.last_valid_limit = max(self.starpilot_planner.params.get_float("PreviousSpeedLimit"), 0.0)
|
self.denied_target = 0
|
||||||
self.last_valid_source = SOURCE_NONE # The persisted number has no known live source.
|
self.map_speed_limit = 0
|
||||||
self.pending_limit = 0.0
|
self.mapbox_limit = 0
|
||||||
self.pending_source = SOURCE_NONE
|
self.next_speed_limit = 0
|
||||||
self.confirmation_time = 0.0
|
self.overridden_speed = 0
|
||||||
self.denied_limit = 0.0
|
self.segment_distance = 0
|
||||||
|
self.speed_limit_changed_timer = 0
|
||||||
|
self.target = 0
|
||||||
|
self.unconfirmed_speed_limit = 0
|
||||||
|
self.vision_limit = 0
|
||||||
|
|
||||||
|
self.previous_source = "None"
|
||||||
|
self.source = "None"
|
||||||
self.previous_road_name = ""
|
self.previous_road_name = ""
|
||||||
|
|
||||||
self.set_speed_override = 0.0
|
self._slc_adopt_counter = 0
|
||||||
self.pedal_override = 0.0
|
|
||||||
self.previous_set_speed = None
|
mapbox_requests_raw = self.starpilot_planner.params.get("MapBoxRequests", encoding="utf-8")
|
||||||
self.consume_set_speed_change = False
|
try:
|
||||||
self.override_disable_time = 0.0
|
self.mapbox_requests = json.loads(mapbox_requests_raw or "{}")
|
||||||
self.limit_change_started = False
|
except (TypeError, ValueError):
|
||||||
self.confirmation_button_consumed = False
|
self.mapbox_requests = {}
|
||||||
self._active_control = False
|
self.mapbox_requests.setdefault("total_requests", 0)
|
||||||
self._using_experimental_fallback = False
|
self.mapbox_requests.setdefault("max_requests", FREE_MAPBOX_REQUESTS - (28 * 100))
|
||||||
self._using_previous_limit_fallback = False
|
|
||||||
self._mode = "off"
|
self.mapbox_host = "https://api.mapbox.com"
|
||||||
|
self.mapbox_token = self.starpilot_planner.params.get("MapboxSecretKey", encoding="utf-8")
|
||||||
|
|
||||||
|
self.previous_target = self.starpilot_planner.params.get_float("PreviousSpeedLimit")
|
||||||
|
self.last_valid_limit = self.previous_target if self.previous_target > 0 else 0
|
||||||
|
|
||||||
|
self.executor = ThreadPoolExecutor(max_workers=1)
|
||||||
|
self.mapbox_future = None
|
||||||
|
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.headers.update({"Accept-Language": "en"})
|
||||||
|
self.session.headers.update({"User-Agent": "starpilot-mapbox-speed-limit-retriever/1.0 (https://github.com/FrogAi/StarPilot)"})
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.mapbox.shutdown()
|
self.executor.shutdown(wait=False, cancel_futures=True)
|
||||||
|
self.session.close()
|
||||||
@property
|
|
||||||
def mapbox_limit(self):
|
|
||||||
return self.mapbox.limit
|
|
||||||
|
|
||||||
@property
|
|
||||||
def confirmation_pending(self):
|
|
||||||
return self.pending_limit >= 1
|
|
||||||
|
|
||||||
@property
|
|
||||||
def unconfirmed_speed_limit(self):
|
|
||||||
return self.pending_limit
|
|
||||||
|
|
||||||
@property
|
|
||||||
def presented_source(self):
|
|
||||||
if self.confirmation_pending:
|
|
||||||
return self.pending_source
|
|
||||||
if self.source in REAL_SOURCES:
|
|
||||||
return self.source
|
|
||||||
if self._using_previous_limit_fallback and self.target >= 1:
|
|
||||||
return self.last_valid_source if self.last_valid_source in REAL_SOURCES else SOURCE_PREVIOUS_LIMIT
|
|
||||||
if (self.denied_limit > 0 and self.last_valid_limit > 0 and self.target >= 1 and
|
|
||||||
abs(self.target - self.last_valid_limit) < SAME_LIMIT_TOLERANCE):
|
|
||||||
return self.last_valid_source if self.last_valid_source in REAL_SOURCES else SOURCE_PREVIOUS_LIMIT
|
|
||||||
return SOURCE_NONE
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def experimental_mode(self):
|
def experimental_mode(self):
|
||||||
return self._active_control and self._using_experimental_fallback
|
return self.target == 0 and bool(getattr(self.starpilot_toggles, "slc_fallback_experimental_mode", False))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_to_use(self):
|
def target_to_use(self):
|
||||||
# Keep Set Speed fallback from arming an override against a higher fake limit.
|
if self.source == "None" and self.target > 0 and self.last_valid_limit > 0:
|
||||||
if self.source == SOURCE_NONE and self.target > 0 and self.last_valid_limit > 0:
|
if self.target >= self.last_valid_limit:
|
||||||
return min(self.target, self.last_valid_limit)
|
return self.last_valid_limit
|
||||||
return self.target
|
return self.target
|
||||||
|
|
||||||
def get_offset(self, limit):
|
def get_offset(self, target_speed):
|
||||||
if self.starpilot_toggles is None:
|
if self.starpilot_toggles is None:
|
||||||
return 0.0
|
return 0
|
||||||
offset_map = OFFSET_MAP_METRIC if self.starpilot_toggles.is_metric else OFFSET_MAP_IMPERIAL
|
offset_map = OFFSET_MAP_METRIC if self.starpilot_toggles.is_metric else OFFSET_MAP_IMPERIAL
|
||||||
return next((getattr(self.starpilot_toggles, name) for low, high, name in offset_map if low <= limit < high), 0.0)
|
return next((getattr(self.starpilot_toggles, offset) for low, high, offset in offset_map if low <= target_speed < high), 0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def offset(self):
|
def offset(self):
|
||||||
@@ -132,303 +124,458 @@ class SpeedLimitController:
|
|||||||
0 < limit <= max(getattr(self.starpilot_toggles, "vision_speed_limit_low_limit_threshold", 0), 0)
|
0 < limit <= max(getattr(self.starpilot_toggles, "vision_speed_limit_low_limit_threshold", 0), 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
def reset_control_state(self):
|
def _confirmation_required(self, desired_source, desired_target):
|
||||||
self._clear_pending()
|
return desired_source != "None" and (
|
||||||
self.clear_override()
|
(desired_target < self.target and self.starpilot_toggles.speed_limit_confirmation_lower) or
|
||||||
self.previous_set_speed = None
|
(desired_target > self.target and self.starpilot_toggles.speed_limit_confirmation_higher)
|
||||||
self.consume_set_speed_change = False
|
)
|
||||||
self.override_disable_time = 0.0
|
|
||||||
self.limit_change_started = False
|
|
||||||
self.confirmation_button_consumed = False
|
|
||||||
self._active_control = False
|
|
||||||
self._using_experimental_fallback = False
|
|
||||||
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
|
|
||||||
self.starpilot_planner.params_memory.remove("SLCAdoptSpeedLimit")
|
|
||||||
|
|
||||||
def _get_vision_limit(self, v_ego, sm, display_only):
|
def clear_override(self):
|
||||||
enabled = getattr(self.starpilot_toggles, "vision_speed_limit_detection", False)
|
self.override_slc = False
|
||||||
self.vision_limit = self.starpilot_planner.params_memory.get_float("VisionSpeedLimit") if enabled else 0.0
|
self.overridden_speed = 0
|
||||||
limit = self.vision_limit
|
self._persistent_override_speed = 0.0
|
||||||
if not display_only and self.low_vision_limit_filtered(limit):
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
|
def clear_persistent_override(self):
|
||||||
|
self._persistent_override_speed = 0.0
|
||||||
|
|
||||||
|
def clear_persistent_override_for_limit_change(self, previous_limit, new_limit):
|
||||||
|
if self._persistent_override_speed <= 0:
|
||||||
|
return
|
||||||
|
if previous_limit <= 0 or new_limit <= 0 or abs(new_limit - previous_limit) < 0.1:
|
||||||
|
return
|
||||||
|
|
||||||
|
new_target_with_offset = new_limit + self.get_offset(new_limit)
|
||||||
|
if new_limit < previous_limit or self._persistent_override_speed <= new_target_with_offset:
|
||||||
|
self.clear_persistent_override()
|
||||||
|
|
||||||
|
def get_mapbox_speed_limit(self, now, time_validated, v_ego, sm):
|
||||||
|
if not self.starpilot_planner.gps_valid or not self.mapbox_token or abs(sm["carState"].steeringAngleDeg - sm["liveParameters"].angleOffsetDeg) >= 45:
|
||||||
|
self.mapbox_limit = 0
|
||||||
|
self.segment_distance = 0
|
||||||
|
return
|
||||||
|
|
||||||
|
if v_ego < 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.segment_distance > 0:
|
||||||
|
self.segment_distance -= v_ego * DT_MDL
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.calling_mapbox:
|
||||||
|
self.segment_distance = v_ego
|
||||||
|
return
|
||||||
|
|
||||||
|
def make_request():
|
||||||
|
successful = False
|
||||||
|
response_data = None
|
||||||
|
try:
|
||||||
|
if not is_url_pingable(self.mapbox_host):
|
||||||
|
self.segment_distance = 1000
|
||||||
|
successful = True
|
||||||
|
return None
|
||||||
|
|
||||||
|
if time_validated:
|
||||||
|
current_month = now.month
|
||||||
|
if current_month != self.mapbox_requests.get("month"):
|
||||||
|
self.mapbox_requests.update({
|
||||||
|
"month": current_month,
|
||||||
|
"total_requests": 0,
|
||||||
|
"max_requests": FREE_MAPBOX_REQUESTS - calendar.monthrange(now.year, current_month)[1] * 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
self.mapbox_requests["total_requests"] += 1
|
||||||
|
self.starpilot_planner.params.put_nonblocking("MapBoxRequests", json.dumps(self.mapbox_requests))
|
||||||
|
|
||||||
|
current_bearing = self.starpilot_planner.gps_position.get("bearing")
|
||||||
|
current_latitude = self.starpilot_planner.gps_position.get("latitude")
|
||||||
|
current_longitude = self.starpilot_planner.gps_position.get("longitude")
|
||||||
|
|
||||||
|
future_latitude, future_longitude = calculate_bearing_offset(current_latitude, current_longitude, current_bearing, v_ego)
|
||||||
|
|
||||||
|
url = (
|
||||||
|
f"{self.mapbox_host}/matching/v5/mapbox/driving/"
|
||||||
|
f"{current_longitude},{current_latitude};"
|
||||||
|
f"{future_longitude},{future_latitude}.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
mapbox_params = {
|
||||||
|
"access_token": self.mapbox_token,
|
||||||
|
"annotations": "maxspeed,distance",
|
||||||
|
"geometries": "polyline6",
|
||||||
|
"overview": "full",
|
||||||
|
"steps": "false",
|
||||||
|
"radiuses": "10;10",
|
||||||
|
"tidy": "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.session.get(url, params=mapbox_params, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
successful = True
|
||||||
|
response_data = response.json()
|
||||||
|
except Exception as exception:
|
||||||
|
print(f"Unexpected error in Mapbox request: {exception}")
|
||||||
|
finally:
|
||||||
|
self.calling_mapbox = False
|
||||||
|
|
||||||
|
if not successful:
|
||||||
|
self.mapbox_limit = 0
|
||||||
|
self.segment_distance = v_ego
|
||||||
|
return response_data
|
||||||
|
|
||||||
|
def complete_request(future):
|
||||||
|
try:
|
||||||
|
data = future.result()
|
||||||
|
if data:
|
||||||
|
matchings = data.get("matchings") or []
|
||||||
|
if not matchings:
|
||||||
|
self.mapbox_limit = 0
|
||||||
|
self.segment_distance = v_ego
|
||||||
|
return
|
||||||
|
|
||||||
|
legs = (matchings[0] or {}).get("legs") or []
|
||||||
|
if not legs:
|
||||||
|
self.mapbox_limit = 0
|
||||||
|
self.segment_distance = v_ego
|
||||||
|
return
|
||||||
|
|
||||||
|
annotation = legs[0].get("annotation") or {}
|
||||||
|
|
||||||
|
distances = annotation.get("distance") or [v_ego]
|
||||||
|
segment_distance = distances[0]
|
||||||
|
|
||||||
|
speed_data = annotation.get("maxspeed", [])
|
||||||
|
if speed_data:
|
||||||
|
first_segment_speed = speed_data[0]
|
||||||
|
try:
|
||||||
|
raw_speed = float(first_segment_speed.get("speed") if first_segment_speed.get("speed") != "none" else 0.0)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
raw_speed = 0.0
|
||||||
|
unit = first_segment_speed.get("unit", "km/h")
|
||||||
|
if raw_speed > 0:
|
||||||
|
if unit == "mph":
|
||||||
|
self.mapbox_limit = raw_speed * CV.MPH_TO_MS
|
||||||
|
else:
|
||||||
|
self.mapbox_limit = raw_speed * CV.KPH_TO_MS
|
||||||
|
self.segment_distance = segment_distance
|
||||||
|
return
|
||||||
|
|
||||||
|
self.mapbox_limit = 0
|
||||||
|
self.segment_distance = v_ego
|
||||||
|
|
||||||
|
except Exception as exception:
|
||||||
|
print(f"Mapbox Callback Error: {exception}")
|
||||||
|
self.mapbox_limit = 0
|
||||||
|
self.segment_distance = v_ego
|
||||||
|
finally:
|
||||||
|
self.mapbox_future = None
|
||||||
|
|
||||||
|
self.calling_mapbox = True
|
||||||
|
try:
|
||||||
|
future = self.executor.submit(make_request)
|
||||||
|
except RuntimeError:
|
||||||
|
self.calling_mapbox = False
|
||||||
|
self.segment_distance = v_ego
|
||||||
|
return
|
||||||
|
|
||||||
|
self.mapbox_future = future
|
||||||
|
future.add_done_callback(complete_request)
|
||||||
|
|
||||||
|
def handle_limit_change(self, desired_source, desired_target, current_road_name, v_ego, sm):
|
||||||
|
self.speed_limit_changed_timer += DT_MDL
|
||||||
|
previous_limit = self.last_valid_limit if self.last_valid_limit > 0 else self.target
|
||||||
|
|
||||||
|
long_active = sm["carControl"].longActive
|
||||||
|
accepted_by_accel_button = sm["starpilotCarState"].accelPressed and long_active
|
||||||
|
confirmation_required = self._confirmation_required(desired_source, desired_target)
|
||||||
|
higher_confirmation = confirmation_required and desired_target > self.target
|
||||||
|
speed_limit_accepted = confirmation_required and accepted_by_accel_button
|
||||||
|
if confirmation_required and not speed_limit_accepted and self._slc_adopt_counter % 4 == 0:
|
||||||
|
speed_limit_accepted = self.starpilot_planner.params_memory.get_bool("SpeedLimitAccepted")
|
||||||
|
if not confirmation_required:
|
||||||
|
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
|
||||||
|
self.unconfirmed_speed_limit = 0
|
||||||
|
speed_limit_denied = confirmation_required and (
|
||||||
|
sm["starpilotCarState"].decelPressed or (self.speed_limit_changed_timer >= 30 and long_active)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not long_active and not sm["selfdriveState"].enabled:
|
||||||
|
speed_limit_accepted = True
|
||||||
|
|
||||||
|
if speed_limit_accepted:
|
||||||
|
self.source = desired_source
|
||||||
|
self.target = desired_target
|
||||||
|
self.clear_persistent_override_for_limit_change(previous_limit, desired_target)
|
||||||
|
set_speed_kph = float(sm["carState"].vCruise)
|
||||||
|
target_with_offset = self.target + self.offset
|
||||||
|
if (
|
||||||
|
higher_confirmation
|
||||||
|
and long_active
|
||||||
|
and 0 < set_speed_kph < V_CRUISE_UNSET
|
||||||
|
and set_speed_kph * CV.KPH_TO_MS < target_with_offset
|
||||||
|
):
|
||||||
|
self.starpilot_planner.params_memory.put_float("SLCForceCruiseSpeed", target_with_offset)
|
||||||
|
if accepted_by_accel_button and confirmation_required:
|
||||||
|
self._set_speed_override_input_consumed = True
|
||||||
|
|
||||||
|
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
|
||||||
|
|
||||||
|
elif speed_limit_denied:
|
||||||
|
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
|
||||||
|
self.denied_target = desired_target
|
||||||
|
|
||||||
|
self.previous_source = desired_source
|
||||||
|
self.previous_target = desired_target
|
||||||
|
self.previous_road_name = current_road_name
|
||||||
|
|
||||||
|
elif desired_target != self.target and not confirmation_required:
|
||||||
|
self.source = desired_source
|
||||||
|
self.target = desired_target
|
||||||
|
self.clear_persistent_override_for_limit_change(previous_limit, desired_target)
|
||||||
|
|
||||||
|
elif desired_target == self.target:
|
||||||
|
self.source = desired_source
|
||||||
|
self.target = desired_target
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.source = "None"
|
||||||
|
self.unconfirmed_speed_limit = desired_target
|
||||||
|
|
||||||
|
if (self.target != self.previous_target or self.previous_road_name != current_road_name) and self.target > 0 and not speed_limit_denied:
|
||||||
|
self.denied_target = 0
|
||||||
|
|
||||||
|
self.previous_source = self.source
|
||||||
|
self.previous_target = self.target
|
||||||
|
self.previous_road_name = current_road_name
|
||||||
|
|
||||||
|
self.starpilot_planner.params.put_nonblocking("PreviousSpeedLimit", float(self.target))
|
||||||
|
|
||||||
|
def update_limits(self, dashboard_speed_limit, now, time_validated, v_cruise, v_ego, sm, display_only=False):
|
||||||
|
self.update_map_speed_limit(v_ego, sm)
|
||||||
|
vision_enabled = getattr(self.starpilot_toggles, "vision_speed_limit_detection", False)
|
||||||
|
self.vision_limit = self.starpilot_planner.params_memory.get_float("VisionSpeedLimit") if vision_enabled else 0
|
||||||
|
usable_vision_limit = self.vision_limit
|
||||||
|
if not display_only and self.low_vision_limit_filtered(usable_vision_limit):
|
||||||
|
usable_vision_limit = 0
|
||||||
|
# The planner clamps V_CRUISE_UNSET to V_CRUISE_MAX, so plausibility must use the raw selected speed.
|
||||||
raw_set_speed_kph = float(sm["carState"].vCruise)
|
raw_set_speed_kph = float(sm["carState"].vCruise)
|
||||||
selected_speed = raw_set_speed_kph * CV.KPH_TO_MS if 0 < raw_set_speed_kph < V_CRUISE_UNSET else 0.0
|
selected_set_speed = raw_set_speed_kph * CV.KPH_TO_MS if 0 < raw_set_speed_kph < V_CRUISE_UNSET else 0
|
||||||
reference_speed = selected_speed if selected_speed > 0 else max(float(v_ego), 0.0)
|
reference_speed = selected_set_speed if selected_set_speed > 0 else max(float(v_ego), 0)
|
||||||
if (limit > 0 and reference_speed > 0 and not sm["carState"].standstill and
|
# vEgo jitters around zero at standstill; do not let that switch the active source.
|
||||||
abs(limit - reference_speed) >= VISION_LARGE_REFERENCE_SPEED_DELTA):
|
if (
|
||||||
memory = self.starpilot_planner.params_memory
|
usable_vision_limit > 0 and reference_speed > 0 and not sm["carState"].standstill and
|
||||||
count = memory.get_int("VisionSpeedLimitSupportCount")
|
abs(usable_vision_limit - reference_speed) >= VISION_LARGE_REFERENCE_SPEED_DELTA
|
||||||
support_speed = memory.get_float("VisionSpeedLimitSupportSpeed")
|
):
|
||||||
if count < VISION_LARGE_SET_SPEED_MIN_SUPPORT or abs(support_speed - limit) > VISION_SUPPORT_SPEED_TOLERANCE:
|
support_count = self.starpilot_planner.params_memory.get_int("VisionSpeedLimitSupportCount")
|
||||||
return 0.0
|
support_speed = self.starpilot_planner.params_memory.get_float("VisionSpeedLimitSupportSpeed")
|
||||||
return limit
|
if support_count < VISION_LARGE_SET_SPEED_MIN_SUPPORT or abs(support_speed - usable_vision_limit) > VISION_SUPPORT_SPEED_TOLERANCE:
|
||||||
|
usable_vision_limit = 0
|
||||||
|
|
||||||
def _update_map_speed_limit(self, v_ego, sm):
|
configured_priorities = {
|
||||||
map_data = sm["mapdOut"]
|
self.starpilot_toggles.speed_limit_priority1,
|
||||||
way_sel = map_data.waySelectionType
|
self.starpilot_toggles.speed_limit_priority2,
|
||||||
if way_sel in (custom.WaySelectionType.current, custom.WaySelectionType.extended):
|
}
|
||||||
self.map_speed_limit = map_data.speedLimit
|
limits = {
|
||||||
self.next_speed_limit = map_data.nextSpeedLimit
|
"Dashboard": dashboard_speed_limit,
|
||||||
elif way_sel in (custom.WaySelectionType.predicted, custom.WaySelectionType.possible):
|
"Map Data": self.map_speed_limit,
|
||||||
speed = map_data.speedLimit
|
}
|
||||||
|
if "Vision" in configured_priorities:
|
||||||
|
limits["Vision"] = usable_vision_limit
|
||||||
|
filtered_limits = {source: limit for source, limit in limits.items() if limit >= 1}
|
||||||
|
|
||||||
|
if self.starpilot_toggles.speed_limit_priority_highest:
|
||||||
|
desired_source = max(filtered_limits, key=filtered_limits.get, default="None")
|
||||||
|
desired_target = filtered_limits.get(desired_source, 0)
|
||||||
|
|
||||||
|
elif self.starpilot_toggles.speed_limit_priority_lowest:
|
||||||
|
desired_source = min(filtered_limits, key=filtered_limits.get, default="None")
|
||||||
|
desired_target = filtered_limits.get(desired_source, 0)
|
||||||
|
|
||||||
|
elif filtered_limits:
|
||||||
|
for priority in [
|
||||||
|
self.starpilot_toggles.speed_limit_priority1,
|
||||||
|
self.starpilot_toggles.speed_limit_priority2
|
||||||
|
]:
|
||||||
|
if priority in filtered_limits:
|
||||||
|
desired_source = priority
|
||||||
|
desired_target = filtered_limits[desired_source]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
desired_source = "None"
|
||||||
|
desired_target = 0
|
||||||
|
|
||||||
|
else:
|
||||||
|
desired_source = "None"
|
||||||
|
desired_target = 0
|
||||||
|
|
||||||
|
if desired_target == 0:
|
||||||
|
if self.mapbox_requests["total_requests"] < self.mapbox_requests["max_requests"] and self.starpilot_toggles.slc_mapbox_filler:
|
||||||
|
self.get_mapbox_speed_limit(now, time_validated, v_ego, sm)
|
||||||
|
|
||||||
|
if self.mapbox_limit >= 1:
|
||||||
|
desired_source = "Mapbox"
|
||||||
|
desired_target = self.mapbox_limit
|
||||||
|
|
||||||
|
if not display_only and desired_target == 0:
|
||||||
|
previous_vision_limit_filtered = self.previous_source == "Vision" and self.low_vision_limit_filtered(self.previous_target)
|
||||||
|
if self.previous_target > 0 and self.starpilot_toggles.slc_fallback_previous_speed_limit and not previous_vision_limit_filtered:
|
||||||
|
desired_source = self.previous_source
|
||||||
|
desired_target = self.previous_target
|
||||||
|
|
||||||
|
self.target = desired_target
|
||||||
|
|
||||||
|
elif sm["selfdriveState"].enabled and self.starpilot_toggles.slc_fallback_set_speed:
|
||||||
|
desired_source = "None"
|
||||||
|
desired_target = v_cruise
|
||||||
|
else:
|
||||||
|
self.mapbox_limit = 0
|
||||||
|
self.segment_distance = 0
|
||||||
|
|
||||||
|
if display_only:
|
||||||
|
self.speed_limit_changed_timer = 0
|
||||||
|
self.unconfirmed_speed_limit = 0
|
||||||
|
self.clear_override()
|
||||||
|
|
||||||
|
if desired_target >= 1:
|
||||||
|
self.source = desired_source
|
||||||
|
self.target = desired_target
|
||||||
|
else:
|
||||||
|
self.source = "None"
|
||||||
|
self.target = 0
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
current_road_name = sm["mapdOut"].roadName if desired_source == "Map Data" else ""
|
||||||
|
current_speed = self.target if (self.source != "None" and self.target > 0) else self.last_valid_limit
|
||||||
|
|
||||||
|
# Do not trigger alerts when shifting to fallback or when re-obtaining the same speed limit
|
||||||
|
is_fallback = desired_source == "None" or desired_target == 0
|
||||||
|
same_speed = desired_target > 0 and current_speed > 0 and abs(desired_target - current_speed) < 1
|
||||||
|
confirmation_required = self._confirmation_required(desired_source, desired_target)
|
||||||
|
denied_same_limit = (
|
||||||
|
confirmation_required and self.denied_target > 0 and
|
||||||
|
abs(desired_target - self.denied_target) < 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if not denied_same_limit:
|
||||||
|
self.denied_target = 0
|
||||||
|
|
||||||
|
if denied_same_limit:
|
||||||
|
self.speed_limit_changed_timer = 0
|
||||||
|
self.unconfirmed_speed_limit = 0
|
||||||
|
elif not is_fallback and not same_speed and (abs(desired_target - self.previous_target) >= 1 or current_speed == 0):
|
||||||
|
self.handle_limit_change(desired_source, desired_target, current_road_name, v_ego, sm)
|
||||||
|
else:
|
||||||
|
self.speed_limit_changed_timer = 0
|
||||||
|
self.unconfirmed_speed_limit = 0
|
||||||
|
if desired_source != self.source or desired_target != self.target:
|
||||||
|
if not is_fallback:
|
||||||
|
self.clear_persistent_override_for_limit_change(current_speed, desired_target)
|
||||||
|
self.source = desired_source
|
||||||
|
self.target = desired_target
|
||||||
|
if desired_source != "None" and desired_target > 0:
|
||||||
|
self.previous_source = desired_source
|
||||||
|
self.previous_target = desired_target
|
||||||
|
if current_road_name != self.previous_road_name and current_road_name != "":
|
||||||
|
self.previous_road_name = current_road_name
|
||||||
|
self.denied_target = 0
|
||||||
|
|
||||||
|
if self.source != "None" and self.target > 0:
|
||||||
|
self.last_valid_limit = self.target
|
||||||
|
|
||||||
|
self._slc_adopt_counter += 1
|
||||||
|
if self._slc_adopt_counter % 4 == 0 and self.starpilot_planner.params_memory.get_bool("SLCAdoptSpeedLimit"):
|
||||||
|
self.starpilot_planner.params_memory.remove("SLCAdoptSpeedLimit")
|
||||||
|
if desired_target > 0:
|
||||||
|
self.clear_override()
|
||||||
|
self.denied_target = 0
|
||||||
|
self.source = desired_source
|
||||||
|
self.target = desired_target
|
||||||
|
self.previous_source = desired_source
|
||||||
|
self.previous_target = desired_target
|
||||||
|
self.speed_limit_changed_timer = 0
|
||||||
|
self.unconfirmed_speed_limit = 0
|
||||||
|
self.starpilot_planner.params.put_nonblocking("PreviousSpeedLimit", float(self.target))
|
||||||
|
self.starpilot_planner.params_memory.put_float("SLCForceCruiseSpeed", self.target + self.offset)
|
||||||
|
|
||||||
|
def update_map_speed_limit(self, v_ego, sm):
|
||||||
|
next_speed_limit_distance = sm["mapdOut"].nextSpeedLimitDistance
|
||||||
|
|
||||||
|
way_sel = sm["mapdOut"].waySelectionType
|
||||||
|
if way_sel in (custom.WaySelectionType.current,
|
||||||
|
custom.WaySelectionType.extended):
|
||||||
|
self.map_speed_limit = sm["mapdOut"].speedLimit
|
||||||
|
self.next_speed_limit = sm["mapdOut"].nextSpeedLimit
|
||||||
|
elif way_sel in (custom.WaySelectionType.predicted,
|
||||||
|
custom.WaySelectionType.possible):
|
||||||
|
speed = sm["mapdOut"].speedLimit
|
||||||
if speed > 0 and (self.map_speed_limit == 0 or speed < self.map_speed_limit):
|
if speed > 0 and (self.map_speed_limit == 0 or speed < self.map_speed_limit):
|
||||||
self.map_speed_limit = speed
|
self.map_speed_limit = speed
|
||||||
self.next_speed_limit = 0.0
|
self.next_speed_limit = 0
|
||||||
else:
|
else:
|
||||||
# Explicit selection failure means the old current limit is no longer live.
|
self.next_speed_limit = 0
|
||||||
self.map_speed_limit = 0.0
|
|
||||||
self.next_speed_limit = 0.0
|
|
||||||
|
|
||||||
if self.next_speed_limit > 0:
|
if self.next_speed_limit > 0:
|
||||||
if self.map_speed_limit < self.next_speed_limit:
|
if self.map_speed_limit < self.next_speed_limit:
|
||||||
lookahead = self.starpilot_toggles.map_speed_lookahead_higher * v_ego
|
max_lookahead = self.starpilot_toggles.map_speed_lookahead_higher * v_ego
|
||||||
elif self.map_speed_limit > self.next_speed_limit:
|
elif self.map_speed_limit > self.next_speed_limit:
|
||||||
lookahead = self.starpilot_toggles.map_speed_lookahead_lower * v_ego
|
max_lookahead = self.starpilot_toggles.map_speed_lookahead_lower * v_ego
|
||||||
else:
|
else:
|
||||||
lookahead = 0.0
|
max_lookahead = 0
|
||||||
if map_data.nextSpeedLimitDistance < lookahead:
|
|
||||||
|
if next_speed_limit_distance < max_lookahead:
|
||||||
self.map_speed_limit = self.next_speed_limit
|
self.map_speed_limit = self.next_speed_limit
|
||||||
|
|
||||||
def _select_limit(self, dashboard, map_limit, vision_limit):
|
def update_override(self, v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm):
|
||||||
priorities = (self.starpilot_toggles.speed_limit_priority1, self.starpilot_toggles.speed_limit_priority2)
|
# Detect +/- changes on the raw set speed (button-driven, no cluster jitter). A fresh edge
|
||||||
limits = {SOURCE_DASHBOARD: dashboard, SOURCE_MAP: map_limit}
|
# keeps a cleared override from re-arming while the selected speed stays high.
|
||||||
if SOURCE_VISION in priorities:
|
prev_v_cruise = self._prev_v_cruise
|
||||||
limits[SOURCE_VISION] = vision_limit
|
self._prev_v_cruise = v_cruise
|
||||||
valid = {source: limit for source, limit in limits.items() if limit >= 1}
|
set_speed_changed = prev_v_cruise is not None and abs(v_cruise - prev_v_cruise) > SET_SPEED_RAISE_EPS
|
||||||
if not valid:
|
set_speed_raised = prev_v_cruise is not None and v_cruise > prev_v_cruise + SET_SPEED_RAISE_EPS
|
||||||
return SOURCE_NONE, 0.0
|
set_speed_input_consumed = self._set_speed_override_input_consumed
|
||||||
if self.starpilot_toggles.speed_limit_priority_highest:
|
# The button and its vCruise update can arrive in adjacent frames. Clear a consumed
|
||||||
source = max(valid, key=valid.get)
|
# confirmation only after this frame has seen the speed change or button release.
|
||||||
elif self.starpilot_toggles.speed_limit_priority_lowest:
|
if set_speed_input_consumed and (set_speed_changed or not sm["starpilotCarState"].accelPressed):
|
||||||
source = min(valid, key=valid.get)
|
self._set_speed_override_input_consumed = False
|
||||||
else:
|
|
||||||
source = next((name for name in priorities if name in valid), SOURCE_NONE)
|
|
||||||
return source, valid.get(source, 0.0)
|
|
||||||
|
|
||||||
def _apply_mapbox_filler(self, source, limit, now, time_validated, v_ego, sm):
|
|
||||||
if source != SOURCE_NONE or not self.starpilot_toggles.slc_mapbox_filler:
|
|
||||||
self.mapbox.reset()
|
|
||||||
return source, limit
|
|
||||||
self.mapbox.update(
|
|
||||||
now, time_validated, v_ego, self.starpilot_planner.gps_valid, self.starpilot_planner.gps_position,
|
|
||||||
sm["carState"].steeringAngleDeg, sm["liveParameters"].angleOffsetDeg,
|
|
||||||
)
|
|
||||||
if self.mapbox.limit >= 1:
|
|
||||||
return SOURCE_MAPBOX, self.mapbox.limit
|
|
||||||
return source, limit
|
|
||||||
|
|
||||||
def _apply_fallback(self, v_cruise, enabled):
|
|
||||||
self._using_experimental_fallback = False
|
|
||||||
self._using_previous_limit_fallback = False
|
|
||||||
previous_vision_filtered = self.last_valid_source == SOURCE_VISION and self.low_vision_limit_filtered(self.last_valid_limit)
|
|
||||||
if self.starpilot_toggles.slc_fallback_previous_speed_limit and self.last_valid_limit > 0 and not previous_vision_filtered:
|
|
||||||
self.source = self.last_valid_source
|
|
||||||
self.target = self.last_valid_limit
|
|
||||||
self._using_previous_limit_fallback = True
|
|
||||||
elif enabled and self.starpilot_toggles.slc_fallback_set_speed:
|
|
||||||
self.source = SOURCE_NONE
|
|
||||||
self.target = v_cruise
|
|
||||||
else:
|
|
||||||
self.source = SOURCE_NONE
|
|
||||||
self.target = 0.0
|
|
||||||
self._using_experimental_fallback = bool(self.starpilot_toggles.slc_fallback_experimental_mode)
|
|
||||||
|
|
||||||
def _confirmation_required(self, limit):
|
|
||||||
current = self.last_valid_limit
|
|
||||||
return ((limit < current and self.starpilot_toggles.speed_limit_confirmation_lower) or
|
|
||||||
(limit > current and self.starpilot_toggles.speed_limit_confirmation_higher))
|
|
||||||
|
|
||||||
def _clear_pending(self):
|
|
||||||
self.pending_limit = 0.0
|
|
||||||
self.pending_source = SOURCE_NONE
|
|
||||||
self.confirmation_time = 0.0
|
|
||||||
|
|
||||||
def _reconcile_set_speed_override(self, old_limit, new_limit):
|
|
||||||
if self.set_speed_override <= 0 or old_limit <= 0 or new_limit <= 0 or abs(new_limit - old_limit) < 0.1:
|
|
||||||
return
|
|
||||||
if (new_limit < old_limit or
|
|
||||||
self.set_speed_override <= new_limit + self.get_offset(new_limit) + SET_SPEED_CHANGE_TOLERANCE_METERS_PER_SECOND):
|
|
||||||
self.set_speed_override = 0.0
|
|
||||||
self.overridden_speed = self.pedal_override
|
|
||||||
|
|
||||||
def _accept_limit(self, source, limit, *, persist=True):
|
|
||||||
assert source in REAL_SOURCES and limit >= 1
|
|
||||||
old_limit = self.last_valid_limit
|
|
||||||
self._reconcile_set_speed_override(old_limit, limit)
|
|
||||||
self.source = source
|
|
||||||
self.target = limit
|
|
||||||
self.last_valid_limit = limit
|
|
||||||
self.last_valid_source = source
|
|
||||||
self.denied_limit = 0.0
|
|
||||||
self._clear_pending()
|
|
||||||
if persist and abs(limit - old_limit) >= 0.1:
|
|
||||||
self.starpilot_planner.params.put_nonblocking("PreviousSpeedLimit", float(limit))
|
|
||||||
|
|
||||||
def _reject_limit(self, limit):
|
|
||||||
self.denied_limit = limit
|
|
||||||
self.source = SOURCE_NONE
|
|
||||||
self._clear_pending()
|
|
||||||
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
|
|
||||||
|
|
||||||
def _update_limit(self, source, limit, sm):
|
|
||||||
road_name = sm["mapdOut"].roadName if source == SOURCE_MAP else ""
|
|
||||||
if road_name and road_name != self.previous_road_name:
|
|
||||||
self.denied_limit = 0.0
|
|
||||||
self.previous_road_name = road_name
|
|
||||||
|
|
||||||
if source == SOURCE_NONE:
|
|
||||||
self._clear_pending()
|
|
||||||
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
|
|
||||||
return
|
|
||||||
|
|
||||||
current = self.last_valid_limit
|
|
||||||
if current > 0 and abs(limit - current) < SAME_LIMIT_TOLERANCE:
|
|
||||||
self._accept_limit(source, limit, persist=False)
|
|
||||||
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
|
|
||||||
return
|
|
||||||
|
|
||||||
confirmation_required = self._confirmation_required(limit)
|
|
||||||
if self.denied_limit > 0 and abs(limit - self.denied_limit) < SAME_LIMIT_TOLERANCE:
|
|
||||||
if confirmation_required:
|
|
||||||
self._clear_pending()
|
|
||||||
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
|
|
||||||
self.source = SOURCE_NONE
|
|
||||||
return
|
|
||||||
# Turning confirmation off applies the already-announced candidate.
|
|
||||||
self._accept_limit(source, limit)
|
|
||||||
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
|
|
||||||
return
|
|
||||||
self.denied_limit = 0.0
|
|
||||||
|
|
||||||
if self.pending_limit == 0 or abs(limit - self.pending_limit) >= SAME_LIMIT_TOLERANCE:
|
|
||||||
self.pending_limit = limit
|
|
||||||
self.pending_source = source
|
|
||||||
self.confirmation_time = 0.0
|
|
||||||
self.limit_change_started = True
|
|
||||||
new_pending = True
|
|
||||||
else:
|
|
||||||
self.pending_source = source
|
|
||||||
new_pending = False
|
|
||||||
|
|
||||||
if not confirmation_required:
|
|
||||||
self._accept_limit(source, limit)
|
|
||||||
self.starpilot_planner.params_memory.remove("SpeedLimitAccepted")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.source = SOURCE_NONE
|
|
||||||
self.confirmation_time += DT_MDL
|
|
||||||
long_active = sm["carControl"].longActive
|
|
||||||
accel_accept = bool(sm["starpilotCarState"].accelPressed and long_active)
|
|
||||||
if new_pending and accel_accept:
|
|
||||||
# This press cannot confirm a candidate that was replaced on this frame.
|
|
||||||
self.confirmation_button_consumed = True
|
|
||||||
self.consume_set_speed_change = True
|
|
||||||
memory = self.starpilot_planner.params_memory
|
|
||||||
ui_accept = memory.get_bool("SpeedLimitAccepted")
|
|
||||||
if ui_accept:
|
|
||||||
memory.remove("SpeedLimitAccepted")
|
|
||||||
|
|
||||||
fully_disengaged = not long_active and not sm["selfdriveState"].enabled
|
|
||||||
if ((accel_accept or ui_accept) and not new_pending) or fully_disengaged:
|
|
||||||
pending_limit, pending_source = self.pending_limit, self.pending_source
|
|
||||||
higher = pending_limit > current
|
|
||||||
self._accept_limit(pending_source, pending_limit)
|
|
||||||
if accel_accept:
|
|
||||||
self.consume_set_speed_change = True
|
|
||||||
self.confirmation_button_consumed = True
|
|
||||||
set_speed_kph = float(sm["carState"].vCruise)
|
|
||||||
target_with_offset = self.target + self.offset
|
|
||||||
if (higher and long_active and 0 < set_speed_kph < V_CRUISE_UNSET and
|
|
||||||
set_speed_kph * CV.KPH_TO_MS < target_with_offset):
|
|
||||||
memory.put_float("SLCForceCruiseSpeed", target_with_offset)
|
|
||||||
elif sm["starpilotCarState"].decelPressed or (self.confirmation_time >= 30 and long_active):
|
|
||||||
self._reject_limit(self.pending_limit)
|
|
||||||
|
|
||||||
def _process_adopt_request(self, source, limit):
|
|
||||||
memory = self.starpilot_planner.params_memory
|
|
||||||
if not memory.get_bool("SLCAdoptSpeedLimit"):
|
|
||||||
return
|
|
||||||
memory.remove("SLCAdoptSpeedLimit")
|
|
||||||
if source not in REAL_SOURCES or limit < 1:
|
|
||||||
return
|
|
||||||
self.clear_override()
|
|
||||||
self.consume_set_speed_change = True
|
|
||||||
self._accept_limit(source, limit)
|
|
||||||
memory.put_float("SLCForceCruiseSpeed", self.target + self.offset)
|
|
||||||
|
|
||||||
def clear_override(self):
|
|
||||||
self.set_speed_override = 0.0
|
|
||||||
self.pedal_override = 0.0
|
|
||||||
self.overridden_speed = 0.0
|
|
||||||
|
|
||||||
def _update_override(self, v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm):
|
|
||||||
previous = self.previous_set_speed
|
|
||||||
self.previous_set_speed = v_cruise
|
|
||||||
changed = previous is not None and abs(v_cruise - previous) > SET_SPEED_CHANGE_TOLERANCE_METERS_PER_SECOND
|
|
||||||
raised = previous is not None and v_cruise > previous + SET_SPEED_CHANGE_TOLERANCE_METERS_PER_SECOND
|
|
||||||
consumed = self.consume_set_speed_change
|
|
||||||
if consumed and (changed or not sm["starpilotCarState"].accelPressed):
|
|
||||||
self.consume_set_speed_change = False
|
|
||||||
|
|
||||||
if not sm["selfdriveState"].enabled:
|
if not sm["selfdriveState"].enabled:
|
||||||
self.override_disable_time += DT_MDL
|
self.override_disable_timer += DT_MDL
|
||||||
if self.override_disable_time >= SLC_OVERRIDE_DISABLE_CLEAR_TIME:
|
if self.override_disable_timer >= SLC_OVERRIDE_DISABLE_CLEAR_TIME:
|
||||||
self.clear_override()
|
self.clear_override()
|
||||||
return
|
return
|
||||||
self.override_disable_time = 0.0
|
|
||||||
|
|
||||||
target = self.target_to_use
|
self.override_disable_timer = 0.0
|
||||||
target_with_offset = target + self.get_offset(target)
|
|
||||||
|
target_to_use = self.target_to_use
|
||||||
|
target_with_offset = target_to_use + self.get_offset(target_to_use)
|
||||||
|
|
||||||
set_speed = v_cruise + v_cruise_diff
|
set_speed = v_cruise + v_cruise_diff
|
||||||
bidirectional = getattr(self.starpilot_toggles, "redneck_cruise", False)
|
bidirectional_set_speed = getattr(self.starpilot_toggles, "redneck_cruise", False)
|
||||||
if self.set_speed_override > 0:
|
|
||||||
if bidirectional:
|
if self._persistent_override_speed > 0:
|
||||||
self.set_speed_override = max(set_speed, 0.0)
|
if bidirectional_set_speed:
|
||||||
elif set_speed <= 0 or (target_with_offset > 0 and set_speed <= target_with_offset and
|
if set_speed <= 0:
|
||||||
(self.source != SOURCE_NONE or changed)):
|
self.clear_persistent_override()
|
||||||
self.set_speed_override = 0.0
|
else:
|
||||||
|
self._persistent_override_speed = set_speed
|
||||||
|
elif set_speed <= 0 or (target_with_offset > 0 and set_speed <= target_with_offset and (self.source != "None" or set_speed_changed)):
|
||||||
|
self.clear_persistent_override()
|
||||||
else:
|
else:
|
||||||
self.set_speed_override = set_speed
|
self._persistent_override_speed = set_speed
|
||||||
elif (target_with_offset > 0 and set_speed > 0 and not consumed and
|
elif (
|
||||||
((bidirectional and changed) or (not bidirectional and raised and set_speed > target_with_offset))):
|
target_with_offset > 0
|
||||||
self.set_speed_override = set_speed
|
and set_speed > 0
|
||||||
|
and not set_speed_input_consumed
|
||||||
|
and ((bidirectional_set_speed and set_speed_changed) or (not bidirectional_set_speed and set_speed_raised and set_speed > target_with_offset))
|
||||||
|
):
|
||||||
|
self._persistent_override_speed = set_speed
|
||||||
|
|
||||||
self.pedal_override = v_ego + v_ego_diff if sm["carState"].gasPressed and v_ego > target_with_offset > 0 else 0.0
|
if sm["carState"].gasPressed and v_ego > target_with_offset > 0:
|
||||||
self.overridden_speed = self.pedal_override or self.set_speed_override
|
self.override_slc = True
|
||||||
|
self.overridden_speed = v_ego + v_ego_diff
|
||||||
def update(self, dashboard_speed_limit, now, time_validated, v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm,
|
elif self._persistent_override_speed > 0:
|
||||||
*, active=True, display_only=False):
|
self.override_slc = True
|
||||||
self.limit_change_started = False
|
self.overridden_speed = self._persistent_override_speed
|
||||||
self.confirmation_button_consumed = False
|
|
||||||
self._using_experimental_fallback = False
|
|
||||||
self._using_previous_limit_fallback = False
|
|
||||||
mode = "display" if display_only else "active" if active else "off"
|
|
||||||
if mode != self._mode:
|
|
||||||
self.mapbox.reset()
|
|
||||||
self._mode = mode
|
|
||||||
if not active and not display_only:
|
|
||||||
self.reset_control_state()
|
|
||||||
self.mapbox.reset()
|
|
||||||
self.source, self.target = SOURCE_NONE, 0.0
|
|
||||||
self.map_speed_limit = self.next_speed_limit = self.vision_limit = 0.0
|
|
||||||
return
|
|
||||||
|
|
||||||
self._update_map_speed_limit(v_ego, sm)
|
|
||||||
vision_limit = self._get_vision_limit(v_ego, sm, display_only)
|
|
||||||
source, limit = self._select_limit(dashboard_speed_limit, self.map_speed_limit, vision_limit)
|
|
||||||
source, limit = self._apply_mapbox_filler(source, limit, now, time_validated, v_ego, sm)
|
|
||||||
|
|
||||||
if display_only:
|
|
||||||
self.reset_control_state()
|
|
||||||
self.source, self.target = (source, limit) if limit >= 1 else (SOURCE_NONE, 0.0)
|
|
||||||
return
|
|
||||||
|
|
||||||
self._active_control = True
|
|
||||||
if source == SOURCE_NONE:
|
|
||||||
self._update_limit(source, limit, sm)
|
|
||||||
self._apply_fallback(v_cruise, sm["selfdriveState"].enabled)
|
|
||||||
else:
|
else:
|
||||||
self._update_limit(source, limit, sm)
|
self.clear_override()
|
||||||
self._process_adopt_request(source, limit)
|
|
||||||
self._update_override(v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm)
|
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ class StarPilotAcceleration:
|
|||||||
|
|
||||||
effective_slc_target = get_active_slc_control_target(
|
effective_slc_target = get_active_slc_control_target(
|
||||||
getattr(starpilot_toggles, "speed_limit_controller", False),
|
getattr(starpilot_toggles, "speed_limit_controller", False),
|
||||||
|
getattr(starpilot_toggles, "set_speed_limit", False),
|
||||||
getattr(self.starpilot_planner.starpilot_vcruise, "slc_target", 0.0),
|
getattr(self.starpilot_planner.starpilot_vcruise, "slc_target", 0.0),
|
||||||
getattr(self.starpilot_planner.starpilot_vcruise, "slc_offset", 0.0),
|
getattr(self.starpilot_planner.starpilot_vcruise, "slc_offset", 0.0),
|
||||||
getattr(getattr(self.starpilot_planner.starpilot_vcruise, "slc", None), "overridden_speed", 0.0),
|
getattr(getattr(self.starpilot_planner.starpilot_vcruise, "slc", None), "overridden_speed", 0.0),
|
||||||
@@ -270,6 +271,7 @@ class StarPilotAcceleration:
|
|||||||
v_ego_diff = v_ego_cluster - v_ego
|
v_ego_diff = v_ego_cluster - v_ego
|
||||||
effective_slc_target = get_active_slc_control_target(
|
effective_slc_target = get_active_slc_control_target(
|
||||||
getattr(starpilot_toggles, "speed_limit_controller", False),
|
getattr(starpilot_toggles, "speed_limit_controller", False),
|
||||||
|
getattr(starpilot_toggles, "set_speed_limit", False),
|
||||||
getattr(self.starpilot_planner.starpilot_vcruise, "slc_target", 0.0),
|
getattr(self.starpilot_planner.starpilot_vcruise, "slc_target", 0.0),
|
||||||
getattr(self.starpilot_planner.starpilot_vcruise, "slc_offset", 0.0),
|
getattr(self.starpilot_planner.starpilot_vcruise, "slc_offset", 0.0),
|
||||||
getattr(getattr(self.starpilot_planner.starpilot_vcruise, "slc", None), "overridden_speed", 0.0),
|
getattr(getattr(self.starpilot_planner.starpilot_vcruise, "slc", None), "overridden_speed", 0.0),
|
||||||
@@ -310,6 +312,7 @@ class StarPilotAcceleration:
|
|||||||
v_ego_cluster = v_ego
|
v_ego_cluster = v_ego
|
||||||
effective_slc_target = get_active_slc_control_target(
|
effective_slc_target = get_active_slc_control_target(
|
||||||
getattr(starpilot_toggles, "speed_limit_controller", False),
|
getattr(starpilot_toggles, "speed_limit_controller", False),
|
||||||
|
getattr(starpilot_toggles, "set_speed_limit", False),
|
||||||
getattr(self.starpilot_planner.starpilot_vcruise, "slc_target", 0.0),
|
getattr(self.starpilot_planner.starpilot_vcruise, "slc_target", 0.0),
|
||||||
getattr(self.starpilot_planner.starpilot_vcruise, "slc_offset", 0.0),
|
getattr(self.starpilot_planner.starpilot_vcruise, "slc_offset", 0.0),
|
||||||
getattr(getattr(self.starpilot_planner.starpilot_vcruise, "slc", None), "overridden_speed", 0.0),
|
getattr(getattr(self.starpilot_planner.starpilot_vcruise, "slc", None), "overridden_speed", 0.0),
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ class StarPilotEvents:
|
|||||||
else:
|
else:
|
||||||
self.events.add(StarPilotEventName.openpilotCrashed)
|
self.events.add(StarPilotEventName.openpilotCrashed)
|
||||||
|
|
||||||
if self.starpilot_planner.starpilot_vcruise.slc.limit_change_started and starpilot_toggles.speed_limit_changed_alert:
|
if self.starpilot_planner.starpilot_vcruise.slc.speed_limit_changed_timer == DT_MDL and starpilot_toggles.speed_limit_changed_alert:
|
||||||
self.events.add(StarPilotEventName.speedLimitChanged)
|
self.events.add(StarPilotEventName.speedLimitChanged)
|
||||||
|
|
||||||
self.startup_seen |= sm["starpilotSelfdriveState"].alertText1 == starpilot_toggles.startup_alert_top and sm["starpilotSelfdriveState"].alertText2 == starpilot_toggles.startup_alert_bottom
|
self.startup_seen |= sm["starpilotSelfdriveState"].alertText1 == starpilot_toggles.startup_alert_top and sm["starpilotSelfdriveState"].alertText2 == starpilot_toggles.startup_alert_bottom
|
||||||
|
|||||||
@@ -105,9 +105,10 @@ def get_lead_veto_distance(car_params):
|
|||||||
return LEAD_VETO_M_OVERRIDES.get(fingerprint, LEAD_VETO_M)
|
return LEAD_VETO_M_OVERRIDES.get(fingerprint, LEAD_VETO_M)
|
||||||
|
|
||||||
|
|
||||||
def get_active_slc_control_target(speed_limit_controller, slc_target, slc_offset, overridden_speed,
|
def get_active_slc_control_target(speed_limit_controller, set_speed_limit, slc_target, slc_offset, overridden_speed,
|
||||||
v_ego_diff, allow_lower_override=False):
|
v_ego_diff, allow_lower_override=False):
|
||||||
# SetSpeedLimit controls engage-time initialization; SLC limits ongoing cruise.
|
# `SetSpeedLimit` only controls engage-time set-speed initialization. Ongoing
|
||||||
|
# SLC speed matching must remain active whenever Speed Limit Controller is on.
|
||||||
if not speed_limit_controller:
|
if not speed_limit_controller:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
@@ -208,7 +209,6 @@ class StarPilotVCruise:
|
|||||||
self._nav_instruction_state_raw = None
|
self._nav_instruction_state_raw = None
|
||||||
self._nav_instruction_state = {}
|
self._nav_instruction_state = {}
|
||||||
self._applied_slc_control_target = 0.0
|
self._applied_slc_control_target = 0.0
|
||||||
self.slc_is_limiting_max_set = False
|
|
||||||
self.csc_controlling_speed = False
|
self.csc_controlling_speed = False
|
||||||
self.csc_glow_release_timer = 0.0
|
self.csc_glow_release_timer = 0.0
|
||||||
self.csc_override = False
|
self.csc_override = False
|
||||||
@@ -343,7 +343,6 @@ class StarPilotVCruise:
|
|||||||
# ===== Main update =====
|
# ===== Main update =====
|
||||||
|
|
||||||
def update(self, controls_enabled, now, time_validated, v_cruise, v_ego, sm, starpilot_toggles):
|
def update(self, controls_enabled, now, time_validated, v_cruise, v_ego, sm, starpilot_toggles):
|
||||||
self.slc_is_limiting_max_set = False
|
|
||||||
if not controls_enabled or not getattr(starpilot_toggles, "speed_limit_controller", False):
|
if not controls_enabled or not getattr(starpilot_toggles, "speed_limit_controller", False):
|
||||||
self._applied_slc_control_target = 0.0
|
self._applied_slc_control_target = 0.0
|
||||||
|
|
||||||
@@ -526,9 +525,6 @@ class StarPilotVCruise:
|
|||||||
v_ego <= force_stop_low_speed_hold and
|
v_ego <= force_stop_low_speed_hold and
|
||||||
v_ego < self.force_stop_entry_speed - 0.25
|
v_ego < self.force_stop_entry_speed - 0.25
|
||||||
)
|
)
|
||||||
# The Santa Fe's model stop signal can blink off after the car has already
|
|
||||||
# committed to the stop. Do not turn that late dropout into a throttle
|
|
||||||
# release while the vehicle is still rolling through the sign.
|
|
||||||
light_stop_cleared &= not low_speed_stop_commit
|
light_stop_cleared &= not low_speed_stop_commit
|
||||||
if light_stop_cleared:
|
if light_stop_cleared:
|
||||||
if self.force_stop_light_clear_since is None:
|
if self.force_stop_light_clear_since is None:
|
||||||
@@ -569,18 +565,6 @@ class StarPilotVCruise:
|
|||||||
v_ego_cluster = max(sm["carState"].vEgoCluster, v_ego)
|
v_ego_cluster = max(sm["carState"].vEgoCluster, v_ego)
|
||||||
v_ego_diff = v_ego_cluster - v_ego
|
v_ego_diff = v_ego_cluster - v_ego
|
||||||
|
|
||||||
# Resolve this frame's SLC confirmation before CSC can consume accel/+.
|
|
||||||
self.slc.starpilot_toggles = starpilot_toggles
|
|
||||||
slc_active = starpilot_toggles.speed_limit_controller
|
|
||||||
slc_display_only = not slc_active and starpilot_toggles.show_speed_limits
|
|
||||||
self.slc.update(
|
|
||||||
sm["starpilotCarState"].dashboardSpeedLimit, now, time_validated,
|
|
||||||
v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm,
|
|
||||||
active=slc_active, display_only=slc_display_only,
|
|
||||||
)
|
|
||||||
self.slc_offset = self.slc.offset if slc_active else 0
|
|
||||||
self.slc_target = self.slc.target if (slc_active or slc_display_only) else 0
|
|
||||||
|
|
||||||
# Curve Speed Controller
|
# Curve Speed Controller
|
||||||
following_lead = bool(getattr(self.starpilot_planner.starpilot_following, "following_lead", False))
|
following_lead = bool(getattr(self.starpilot_planner.starpilot_following, "following_lead", False))
|
||||||
manual_speed_control = is_manual_speed_control(sm)
|
manual_speed_control = is_manual_speed_control(sm)
|
||||||
@@ -599,9 +583,8 @@ class StarPilotVCruise:
|
|||||||
not self.starpilot_planner.driving_in_curve)
|
not self.starpilot_planner.driving_in_curve)
|
||||||
csc_was_controlling = self.csc_controlling_speed
|
csc_was_controlling = self.csc_controlling_speed
|
||||||
|
|
||||||
csc_accel_button = (bool(sm["starpilotCarState"].accelPressed) and
|
slc_confirmation_pending = self.slc.speed_limit_changed_timer > DT_MDL and self.slc.unconfirmed_speed_limit >= 1
|
||||||
not self.slc.confirmation_pending and
|
csc_accel_button = bool(sm["starpilotCarState"].accelPressed) and not slc_confirmation_pending
|
||||||
not self.slc.confirmation_button_consumed)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -651,6 +634,24 @@ class StarPilotVCruise:
|
|||||||
self.csc.handle_override(v_ego, csc_was_controlling, sm, accel_button=csc_accel_button)
|
self.csc.handle_override(v_ego, csc_was_controlling, sm, accel_button=csc_accel_button)
|
||||||
self.csc.log_data(v_ego, sm)
|
self.csc.log_data(v_ego, sm)
|
||||||
|
|
||||||
|
# Pfeiferj's Speed Limit Controller
|
||||||
|
self.slc.starpilot_toggles = starpilot_toggles
|
||||||
|
|
||||||
|
if starpilot_toggles.speed_limit_controller:
|
||||||
|
self.slc.update_limits(sm["starpilotCarState"].dashboardSpeedLimit, now, time_validated, v_cruise, v_ego, sm)
|
||||||
|
self.slc.update_override(v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm)
|
||||||
|
|
||||||
|
self.slc_offset = self.slc.offset
|
||||||
|
self.slc_target = self.slc.target
|
||||||
|
elif starpilot_toggles.show_speed_limits:
|
||||||
|
self.slc.update_limits(sm["starpilotCarState"].dashboardSpeedLimit, now, time_validated, v_cruise, v_ego, sm, display_only=True)
|
||||||
|
|
||||||
|
self.slc_offset = 0
|
||||||
|
self.slc_target = self.slc.target
|
||||||
|
else:
|
||||||
|
self.slc_offset = 0
|
||||||
|
self.slc_target = 0
|
||||||
|
|
||||||
self.nav_turn_target = self._get_nav_turn_control_target(v_cruise, sm, starpilot_toggles)
|
self.nav_turn_target = self._get_nav_turn_control_target(v_cruise, sm, starpilot_toggles)
|
||||||
|
|
||||||
# Single tuning knob (signed feet -> meters). Defense clamp on top of UI bounds.
|
# Single tuning knob (signed feet -> meters). Defense clamp on top of UI bounds.
|
||||||
@@ -746,6 +747,7 @@ class StarPilotVCruise:
|
|||||||
targets.append(self.csc_target)
|
targets.append(self.csc_target)
|
||||||
slc_control_target = get_active_slc_control_target(
|
slc_control_target = get_active_slc_control_target(
|
||||||
starpilot_toggles.speed_limit_controller,
|
starpilot_toggles.speed_limit_controller,
|
||||||
|
getattr(starpilot_toggles, "set_speed_limit", False),
|
||||||
self.slc_target,
|
self.slc_target,
|
||||||
self.slc_offset,
|
self.slc_offset,
|
||||||
self.slc.overridden_speed,
|
self.slc.overridden_speed,
|
||||||
@@ -761,8 +763,6 @@ class StarPilotVCruise:
|
|||||||
self.slc.overridden_speed > 0.0,
|
self.slc.overridden_speed > 0.0,
|
||||||
getattr(self.slc, "source", "None"),
|
getattr(self.slc, "source", "None"),
|
||||||
)
|
)
|
||||||
# Publish the semantic used by the UI after the lead-drop adjustment.
|
|
||||||
self.slc_is_limiting_max_set = bool(controls_enabled and 0 < slc_control_target < v_cruise)
|
|
||||||
self._applied_slc_control_target = slc_control_target if slc_control_target > 0.0 else 0.0
|
self._applied_slc_control_target = slc_control_target if slc_control_target > 0.0 else 0.0
|
||||||
if slc_control_target > 0.0:
|
if slc_control_target > 0.0:
|
||||||
targets.append(slc_control_target)
|
targets.append(slc_control_target)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from opendbc.car.chrysler.values import pacifica_hybrid_aol_requires_set_press
|
|||||||
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, HyundaiFlags
|
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, HyundaiFlags
|
||||||
from opendbc.safety import ALTERNATIVE_EXPERIENCE
|
from opendbc.safety import ALTERNATIVE_EXPERIENCE
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.selfdrive.car.cruise import CRUISE_LONG_PRESS, ButtonType, is_speed_limit_confirmation_pending
|
from openpilot.selfdrive.car.cruise import CRUISE_LONG_PRESS, ButtonType
|
||||||
from openpilot.selfdrive.selfdrived.events import ET
|
from openpilot.selfdrive.selfdrived.events import ET
|
||||||
|
|
||||||
from openpilot.starpilot.common.experimental_state import (
|
from openpilot.starpilot.common.experimental_state import (
|
||||||
@@ -57,8 +57,6 @@ class StarPilotCard:
|
|||||||
self.params_memory = Params(memory=True)
|
self.params_memory = Params(memory=True)
|
||||||
|
|
||||||
self.accel_pressed = False
|
self.accel_pressed = False
|
||||||
self.confirmation_button_suppressed = set()
|
|
||||||
self.pressed_accel_buttons = set()
|
|
||||||
self.always_on_lateral_allowed = False
|
self.always_on_lateral_allowed = False
|
||||||
self.controller_aol_override = None
|
self.controller_aol_override = None
|
||||||
self.pacifica_aol_set_seen = False
|
self.pacifica_aol_set_seen = False
|
||||||
@@ -273,23 +271,6 @@ class StarPilotCard:
|
|||||||
]
|
]
|
||||||
|
|
||||||
button_event_types = [self._button_type_raw(be) for be in carState.buttonEvents]
|
button_event_types = [self._button_type_raw(be) for be in carState.buttonEvents]
|
||||||
accel_button_types = (int(ButtonType.accelCruise), int(ButtonType.resumeCruise))
|
|
||||||
confirmation_pending = is_speed_limit_confirmation_pending(sm["starpilotPlan"])
|
|
||||||
if confirmation_pending:
|
|
||||||
self.confirmation_button_suppressed.update(self.pressed_accel_buttons)
|
|
||||||
suppressed_releases = set()
|
|
||||||
for be, be_type in zip(carState.buttonEvents, button_event_types, strict=False):
|
|
||||||
if be_type not in accel_button_types:
|
|
||||||
continue
|
|
||||||
if be.pressed:
|
|
||||||
self.pressed_accel_buttons.add(be_type)
|
|
||||||
if confirmation_pending:
|
|
||||||
self.confirmation_button_suppressed.add(be_type)
|
|
||||||
else:
|
|
||||||
self.pressed_accel_buttons.discard(be_type)
|
|
||||||
if be_type in self.confirmation_button_suppressed:
|
|
||||||
self.confirmation_button_suppressed.remove(be_type)
|
|
||||||
suppressed_releases.add(be_type)
|
|
||||||
button_aol_supported = self.always_on_lateral_supported and (
|
button_aol_supported = self.always_on_lateral_supported and (
|
||||||
self.CP.brand == "hyundai" or starpilot_toggles.lkas_allowed_for_aol
|
self.CP.brand == "hyundai" or starpilot_toggles.lkas_allowed_for_aol
|
||||||
)
|
)
|
||||||
@@ -411,11 +392,8 @@ class StarPilotCard:
|
|||||||
if not self.always_on_lateral_supported:
|
if not self.always_on_lateral_supported:
|
||||||
self.always_on_lateral_allowed = False
|
self.always_on_lateral_allowed = False
|
||||||
|
|
||||||
if sm.updated["starpilotPlan"] or any(be_type in accel_button_types for be_type in button_event_types):
|
if sm.updated["starpilotPlan"] or any(be_type in (ButtonType.accelCruise, ButtonType.resumeCruise) for be_type in button_event_types):
|
||||||
self.accel_pressed = any(
|
self.accel_pressed = any(be_type in (ButtonType.accelCruise, ButtonType.resumeCruise) for be_type in button_event_types)
|
||||||
be_type in accel_button_types and (be.pressed or be_type not in suppressed_releases)
|
|
||||||
for be, be_type in zip(carState.buttonEvents, button_event_types, strict=False)
|
|
||||||
)
|
|
||||||
|
|
||||||
if sm.updated["starpilotPlan"] or any(be_type == ButtonType.decelCruise for be_type in button_event_types):
|
if sm.updated["starpilotPlan"] or any(be_type == ButtonType.decelCruise for be_type in button_event_types):
|
||||||
self.decel_pressed = any(be_type == ButtonType.decelCruise for be_type in button_event_types)
|
self.decel_pressed = any(be_type == ButtonType.decelCruise for be_type in button_event_types)
|
||||||
|
|||||||
@@ -382,9 +382,7 @@ class StarPilotPlanner:
|
|||||||
starpilotPlan.slcSpeedLimit = self.starpilot_vcruise.slc_target
|
starpilotPlan.slcSpeedLimit = self.starpilot_vcruise.slc_target
|
||||||
starpilotPlan.slcSpeedLimitOffset = self.starpilot_vcruise.slc_offset
|
starpilotPlan.slcSpeedLimitOffset = self.starpilot_vcruise.slc_offset
|
||||||
starpilotPlan.slcSpeedLimitSource = self.starpilot_vcruise.slc.source
|
starpilotPlan.slcSpeedLimitSource = self.starpilot_vcruise.slc.source
|
||||||
starpilotPlan.slcPresentedSpeedLimitSource = self.starpilot_vcruise.slc.presented_source
|
starpilotPlan.speedLimitChanged = self.starpilot_vcruise.slc.speed_limit_changed_timer > DT_MDL
|
||||||
starpilotPlan.slcIsLimitingMaxSet = self.starpilot_vcruise.slc_is_limiting_max_set
|
|
||||||
starpilotPlan.speedLimitChanged = self.starpilot_vcruise.slc.confirmation_pending
|
|
||||||
starpilotPlan.unconfirmedSlcSpeedLimit = self.starpilot_vcruise.slc.unconfirmed_speed_limit
|
starpilotPlan.unconfirmedSlcSpeedLimit = self.starpilot_vcruise.slc.unconfirmed_speed_limit
|
||||||
|
|
||||||
starpilotPlan.themeUpdated = theme_updated
|
starpilotPlan.themeUpdated = theme_updated
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ sys.modules["openpilot.selfdrive.controls.lib.longitudinal_planner"] = _module(
|
|||||||
)
|
)
|
||||||
sys.modules["openpilot.starpilot.controls.lib.starpilot_vcruise"] = _module(
|
sys.modules["openpilot.starpilot.controls.lib.starpilot_vcruise"] = _module(
|
||||||
"openpilot.starpilot.controls.lib.starpilot_vcruise",
|
"openpilot.starpilot.controls.lib.starpilot_vcruise",
|
||||||
get_active_slc_control_target=lambda enabled, target, offset, overridden_speed, *_args, **_kwargs: (
|
get_active_slc_control_target=lambda enabled, set_speed_limit, target, offset, overridden_speed, *_args, **_kwargs: (
|
||||||
float(overridden_speed or target) + float(offset) if enabled else 0.0
|
float(overridden_speed or target) + float(offset) if enabled and set_speed_limit else 0.0
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ def make_sm():
|
|||||||
"carControl": SimpleNamespace(longActive=False),
|
"carControl": SimpleNamespace(longActive=False),
|
||||||
"selfdriveState": SimpleNamespace(active=False, alertType=[], experimentalMode=False),
|
"selfdriveState": SimpleNamespace(active=False, alertType=[], experimentalMode=False),
|
||||||
"starpilotSelfdriveState": SimpleNamespace(alertType=[]),
|
"starpilotSelfdriveState": SimpleNamespace(alertType=[]),
|
||||||
"starpilotPlan": SimpleNamespace(lateralCheck=True, speedLimitChanged=False, unconfirmedSlcSpeedLimit=0.0),
|
"starpilotPlan": SimpleNamespace(lateralCheck=True),
|
||||||
"liveCalibration": SimpleNamespace(calPerc=100),
|
"liveCalibration": SimpleNamespace(calPerc=100),
|
||||||
}, updated={"starpilotPlan": False})
|
}, updated={"starpilotPlan": False})
|
||||||
|
|
||||||
@@ -295,37 +295,6 @@ def make_wrapped_button_event(button_type, pressed):
|
|||||||
return SimpleNamespace(type=SimpleNamespace(raw=int(button_type)), pressed=pressed)
|
return SimpleNamespace(type=SimpleNamespace(raw=int(button_type)), pressed=pressed)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("pending_before_press", [False, True])
|
|
||||||
def test_slc_confirmation_release_does_not_republish_accel(monkeypatch, tmp_path, pending_before_press):
|
|
||||||
monkeypatch.setattr(spc, "Params", FakeParams)
|
|
||||||
monkeypatch.setattr(spc, "ERROR_LOGS_PATH", tmp_path)
|
|
||||||
card = spc.StarPilotCard(SimpleNamespace(brand="gm"), SimpleNamespace(alternativeExperience=0))
|
|
||||||
sm = make_sm()
|
|
||||||
toggles = make_toggles(speed_limit_controller=True)
|
|
||||||
starpilot_car_state = SimpleNamespace(distancePressed=False)
|
|
||||||
button_type = spc.ButtonType.accelCruise
|
|
||||||
|
|
||||||
if pending_before_press:
|
|
||||||
sm["starpilotPlan"].speedLimitChanged = True
|
|
||||||
sm["starpilotPlan"].unconfirmedSlcSpeedLimit = 20.0
|
|
||||||
pressed = make_car_state(button_events=[make_wrapped_button_event(button_type, True)])
|
|
||||||
assert card.update(pressed, starpilot_car_state, sm, toggles).accelPressed
|
|
||||||
|
|
||||||
if not pending_before_press:
|
|
||||||
sm["starpilotPlan"].speedLimitChanged = True
|
|
||||||
sm["starpilotPlan"].unconfirmedSlcSpeedLimit = 20.0
|
|
||||||
card.update(make_car_state(), starpilot_car_state, sm, toggles)
|
|
||||||
|
|
||||||
sm["starpilotPlan"].speedLimitChanged = False
|
|
||||||
sm["starpilotPlan"].unconfirmedSlcSpeedLimit = 0.0
|
|
||||||
released = make_car_state(button_events=[make_wrapped_button_event(button_type, False)])
|
|
||||||
assert not card.update(released, starpilot_car_state, sm, toggles).accelPressed
|
|
||||||
assert not card.confirmation_button_suppressed
|
|
||||||
|
|
||||||
assert card.update(pressed, starpilot_car_state, sm, toggles).accelPressed
|
|
||||||
assert card.update(released, starpilot_car_state, sm, toggles).accelPressed
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("car_fingerprint", "expect_normalized_release"),
|
("car_fingerprint", "expect_normalized_release"),
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"""Opt-in, read-only Stinger object logging. Never publishes control or radar data."""
|
||||||
|
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
from collections import deque
|
||||||
|
from dataclasses import asdict
|
||||||
|
|
||||||
|
from openpilot.tools.car_porting.stinger_object_tracks import MAX_EGO_AGE_NS, SLOT_COUNT, StingerObjectDecoder
|
||||||
|
|
||||||
|
|
||||||
|
PLATFORM = "KIA_STINGER_2022"
|
||||||
|
LOG_INTERVAL_NS = 500_000_000
|
||||||
|
MAX_OBJECT_AGE_NS = 100_000_000
|
||||||
|
|
||||||
|
|
||||||
|
class StingerObjectShadow:
|
||||||
|
def __init__(self, openpilot_longitudinal: bool):
|
||||||
|
self.openpilot_longitudinal = openpilot_longitudinal
|
||||||
|
self.decoder = StingerObjectDecoder()
|
||||||
|
self.ego_history = deque(maxlen=128)
|
||||||
|
self.objects: dict[int, dict] = {}
|
||||||
|
self.scc = None
|
||||||
|
self.object_count = 0
|
||||||
|
self.last_log_ns = None
|
||||||
|
|
||||||
|
def update_ego(self, timestamp_ns: int, speed: float, cruise_enabled: bool, brake_pressed: bool, steering_angle: float):
|
||||||
|
if not math.isfinite(speed) or not math.isfinite(steering_angle):
|
||||||
|
return
|
||||||
|
if self.ego_history and timestamp_ns <= self.ego_history[-1]["timestamp_ns"]:
|
||||||
|
return
|
||||||
|
self.ego_history.append({"timestamp_ns": timestamp_ns, "speed": speed, "cruise_enabled": cruise_enabled,
|
||||||
|
"brake_pressed": brake_pressed, "steering_angle": steering_angle})
|
||||||
|
|
||||||
|
def update_can(self, timestamp_ns: int, address: int, dat: bytes, bus: int):
|
||||||
|
if bus == 0 and address == 0x420 and len(dat) == 8:
|
||||||
|
raw = int.from_bytes(dat, "little")
|
||||||
|
self.scc = {"timestamp_ns": timestamp_ns, "payload": dat.hex(), "main_mode": bool(raw & 1),
|
||||||
|
"object_valid": bool((raw >> 16) & 1), "object_status": (raw >> 22) & 3,
|
||||||
|
"distance": ((raw >> 33) & 0x7ff) * 0.1,
|
||||||
|
"relative_speed": ((raw >> 44) & 0xfff) * 0.1 - 170.0}
|
||||||
|
|
||||||
|
obj = self.decoder.update(timestamp_ns, address, dat, bus)
|
||||||
|
if obj is None:
|
||||||
|
return
|
||||||
|
ego = next((sample for sample in reversed(self.ego_history)
|
||||||
|
if 0 <= timestamp_ns - sample["timestamp_ns"] <= MAX_EGO_AGE_NS), None)
|
||||||
|
self.objects[obj.slot] = {"timestamp_ns": timestamp_ns, **asdict(obj),
|
||||||
|
"ego_timestamp_ns": ego["timestamp_ns"] if ego else None,
|
||||||
|
"ego_speed": ego["speed"] if ego else None,
|
||||||
|
"relative_speed": obj.relative_speed(ego["speed"]) if ego else None}
|
||||||
|
self.object_count += 1
|
||||||
|
|
||||||
|
def snapshot(self, timestamp_ns: int) -> dict | None:
|
||||||
|
if self.last_log_ns is not None and timestamp_ns - self.last_log_ns < LOG_INTERVAL_NS:
|
||||||
|
return None
|
||||||
|
self.last_log_ns = timestamp_ns
|
||||||
|
self.objects = {slot: obj for slot, obj in self.objects.items()
|
||||||
|
if 0 <= timestamp_ns - obj["timestamp_ns"] <= MAX_OBJECT_AGE_NS}
|
||||||
|
assert len(self.objects) <= SLOT_COUNT
|
||||||
|
scc = self.scc if self.scc and 0 <= timestamp_ns - self.scc["timestamp_ns"] <= MAX_OBJECT_AGE_NS else None
|
||||||
|
ego = self.ego_history[-1] if self.ego_history else None
|
||||||
|
if ego and not 0 <= timestamp_ns - ego["timestamp_ns"] <= MAX_EGO_AGE_NS:
|
||||||
|
ego = None
|
||||||
|
return {"timestamp_ns": timestamp_ns, "platform": PLATFORM, "shadow_only": True,
|
||||||
|
"openpilot_longitudinal": self.openpilot_longitudinal, "decoded_objects_total": self.object_count,
|
||||||
|
"objects": [self.objects[slot] for slot in sorted(self.objects)], "car_state": ego,
|
||||||
|
"scc11": scc, "scc11_independent_reference": scc is not None and not self.openpilot_longitudinal}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
from cereal import car, messaging
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.common.swaglog import cloudlog
|
||||||
|
|
||||||
|
params = Params()
|
||||||
|
cp_bytes = params.get("CarParams")
|
||||||
|
if cp_bytes is None or not params.get_bool("StingerObjectShadow") or params.get_bool("DisableLogging"):
|
||||||
|
return
|
||||||
|
with car.CarParams.from_bytes(cp_bytes) as CP:
|
||||||
|
if CP.carFingerprint != PLATFORM or CP.notCar:
|
||||||
|
return
|
||||||
|
collector = StingerObjectShadow(CP.openpilotLongitudinalControl)
|
||||||
|
|
||||||
|
can_sock = messaging.sub_sock("can", timeout=100)
|
||||||
|
sm = messaging.SubMaster(["carState"])
|
||||||
|
cloudlog.event("stinger_object_shadow_start", platform=PLATFORM, shadow_only=True)
|
||||||
|
while True:
|
||||||
|
sm.update(0)
|
||||||
|
if sm.updated["carState"] and sm.valid["carState"]:
|
||||||
|
CS = sm["carState"]
|
||||||
|
collector.update_ego(sm.logMonoTime["carState"], CS.vEgo, CS.cruiseState.enabled, CS.brakePressed, CS.steeringAngleDeg)
|
||||||
|
for event in messaging.drain_sock(can_sock, wait_for_one=True):
|
||||||
|
if event.which() == "can" and event.valid:
|
||||||
|
for msg in event.can:
|
||||||
|
collector.update_can(event.logMonoTime, msg.address, bytes(msg.dat), msg.src)
|
||||||
|
snapshot = collector.snapshot(time.monotonic_ns())
|
||||||
|
if snapshot is not None:
|
||||||
|
if not params.get_bool("StingerObjectShadow") or params.get_bool("DisableLogging"):
|
||||||
|
return
|
||||||
|
cloudlog.event("stinger_object_shadow", **snapshot)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -39,6 +39,13 @@ def test_galaxy_does_not_assign_a_regional_label_to_ambiguous_ev6_fingerprint():
|
|||||||
assert catalog["model_to_label"]["KIA_EV6"] is None
|
assert catalog["model_to_label"]["KIA_EV6"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_galaxy_lists_2026_k4_under_existing_non_hda2_platform():
|
||||||
|
kia_models = the_galaxy._extract_fingerprint_models_for_make("kia")
|
||||||
|
assert {"value": "KIA_K4_2025", "label": "Kia K4 (without HDA II) 2025-26"} in kia_models
|
||||||
|
assert {"value": "KIA_K4_2025", "label": "Kia K4 (with HDA II) 2025"} in kia_models
|
||||||
|
assert {"value": "KIA_K4_2025", "label": "Kia K4 (with HDA II) 2025-26"} not in kia_models
|
||||||
|
|
||||||
|
|
||||||
def test_manual_fingerprint_api_keeps_the_saved_value_and_label_consistent(monkeypatch):
|
def test_manual_fingerprint_api_keeps_the_saved_value_and_label_consistent(monkeypatch):
|
||||||
client, params = _params_client(monkeypatch, {}, "pc")
|
client, params = _params_client(monkeypatch, {}, "pc")
|
||||||
monkeypatch.setattr(api_server, "_get_param_type_info", lambda: ({"CarModel"}, {"CarModel": str}))
|
monkeypatch.setattr(api_server, "_get_param_type_info", lambda: ({"CarModel"}, {"CarModel": str}))
|
||||||
|
|||||||
@@ -143,6 +143,12 @@ def run_v_asm(started: bool, params: Params, CP: car.CarParams, starpilot_toggle
|
|||||||
return started and getattr(starpilot_toggles, "v_asm_enabled", False)
|
return started and getattr(starpilot_toggles, "v_asm_enabled", False)
|
||||||
|
|
||||||
|
|
||||||
|
def run_stinger_object_shadow(started: bool, params: Params, CP: car.CarParams, starpilot_toggles: SimpleNamespace) -> bool:
|
||||||
|
return (started and not CP.notCar and CP.carFingerprint == "KIA_STINGER_2022" and
|
||||||
|
params.get_bool("StingerObjectShadow") and not params.get_bool("DisableLogging") and
|
||||||
|
not getattr(starpilot_toggles, "no_logging", False))
|
||||||
|
|
||||||
|
|
||||||
def big_device_ui_process() -> NativeProcess:
|
def big_device_ui_process() -> NativeProcess:
|
||||||
return NativeProcess(
|
return NativeProcess(
|
||||||
"ui",
|
"ui",
|
||||||
@@ -231,6 +237,7 @@ procs += [
|
|||||||
PythonProcess("speed_limit_filler", "starpilot.system.speed_limit_filler", run_speed_limit_filler, nice=19),
|
PythonProcess("speed_limit_filler", "starpilot.system.speed_limit_filler", run_speed_limit_filler, nice=19),
|
||||||
PythonProcess("speed_limit_vision", "starpilot.system.speed_limit_vision", run_speed_limit_vision, nice=19),
|
PythonProcess("speed_limit_vision", "starpilot.system.speed_limit_vision", run_speed_limit_vision, nice=19),
|
||||||
PythonProcess("adj_spot_monitor_vision", "starpilot.system.adj_spot_monitor_vision", run_v_asm, nice=19),
|
PythonProcess("adj_spot_monitor_vision", "starpilot.system.adj_spot_monitor_vision", run_v_asm, nice=19),
|
||||||
|
PythonProcess("stinger_object_shadow", "starpilot.system.stinger_object_shadow", run_stinger_object_shadow, nice=19),
|
||||||
]
|
]
|
||||||
|
|
||||||
managed_processes = {p.name: p for p in procs}
|
managed_processes = {p.name: p for p in procs}
|
||||||
|
|||||||
Reference in New Issue
Block a user