This commit is contained in:
firestar5683
2026-09-28 13:51:49 -05:00
parent 8d01d881cb
commit 96ef704da7
22 changed files with 500 additions and 53 deletions
+26 -7
View File
@@ -1,4 +1,5 @@
import json
from time import monotonic
import numpy as np
@@ -12,8 +13,11 @@ LaneChangeDirection = log.LaneChangeDirection
LANE_CHANGE_SPEED_MIN = 20 * CV.MPH_TO_MS
LANE_CHANGE_TIME_MAX = 10.
NAV_TURN_DISTANCE_SPEED_BREAKPOINTS = [0.0, 5.0, 10.0]
NAV_TURN_DISTANCE_BREAKPOINTS = [20.0, 35.0, 55.0]
NAV_TURN_MAX_SPEED = 14.0
NAV_TURN_PREVIEW_SECONDS = 6.0
NAV_TURN_MIN_DISTANCE = 35.0
NAV_TURN_MAX_DISTANCE = 90.0
NAV_INSTRUCTION_MAX_AGE = 2.5
# A driver normally signals an intersection before slowing below the lane-change
# speed threshold. Use the route to classify that early signal so it does not
# start a lane change while approaching the matching turn.
@@ -122,7 +126,20 @@ class DesireHelper:
except (TypeError, ValueError):
return False
return 0.0 <= distance <= float(np.interp(carstate.vEgo, NAV_TURN_DISTANCE_SPEED_BREAKPOINTS, NAV_TURN_DISTANCE_BREAKPOINTS))
preview_distance = float(np.clip(
max(float(carstate.vEgo), 0.0) * NAV_TURN_PREVIEW_SECONDS,
NAV_TURN_MIN_DISTANCE,
NAV_TURN_MAX_DISTANCE,
))
return 0.0 <= distance <= preview_distance
def _nav_instruction_is_fresh(self):
try:
updated_at = float(self._nav_instruction_state["updatedAtMonotonic"])
except (KeyError, TypeError, ValueError):
return False
age = monotonic() - updated_at
return 0.0 <= age <= NAV_INSTRUCTION_MAX_AGE
@staticmethod
def _nav_turn_signal_matches(carstate, nav_instruction_state):
@@ -234,7 +251,7 @@ class DesireHelper:
self.nav_lane_positioning_allowed = bool(
getattr(starpilot_toggles, "nav_lane_positioning_allowed", self.nav_lane_positioning_allowed)
)
if not self.nav_desires_allowed or not lateral_active or not bool(self._nav_instruction_state.get("valid", False)):
if not self.nav_desires_allowed or not lateral_active or not bool(self._nav_instruction_state.get("valid", False)) or not self._nav_instruction_is_fresh():
return log.Desire.none
maneuver_distance = self._nav_instruction_state.get("maneuverDistance", 0.0)
@@ -265,14 +282,14 @@ class DesireHelper:
if self.turn_stop_hold:
return log.Desire.none
turn_allowed = carstate.leftBlinker and not carstate.rightBlinker and not carstate.leftBlindspot
turn_allowed &= carstate.vEgo < starpilot_toggles.minimum_lane_change_speed and not carstate.standstill
turn_allowed &= 0.0 <= carstate.vEgo < NAV_TURN_MAX_SPEED and not carstate.standstill
if turn_allowed and self._nav_turn_is_imminent(carstate, maneuver_distance):
return log.Desire.turnLeft
elif modifier in ("right", "sharpRight"):
if self.turn_stop_hold:
return log.Desire.none
turn_allowed = carstate.rightBlinker and not carstate.leftBlinker and not carstate.rightBlindspot
turn_allowed &= carstate.vEgo < starpilot_toggles.minimum_lane_change_speed and not carstate.standstill
turn_allowed &= 0.0 <= carstate.vEgo < NAV_TURN_MAX_SPEED and not carstate.standstill
if turn_allowed and self._nav_turn_is_imminent(carstate, maneuver_distance):
return log.Desire.turnRight
@@ -289,7 +306,7 @@ class DesireHelper:
self._update_nav_params()
self.nav_desires_allowed = bool(getattr(starpilot_toggles, "nav_desires_allowed", self.nav_desires_allowed))
nav_turn_signal = self.nav_desires_allowed and self._nav_turn_signal_matches(carstate, self._nav_instruction_state)
nav_turn_signal = self.nav_desires_allowed and self._nav_instruction_is_fresh() and self._nav_turn_signal_matches(carstate, self._nav_instruction_state)
stop_imminent = (bool(getattr(starpilotPlan, "redLight", False))
or bool(getattr(starpilotPlan, "forcingStop", False))
@@ -409,3 +426,5 @@ class DesireHelper:
nav_desire = self._navigation_desire(carstate, lateral_active, starpilotPlan, starpilot_toggles)
if nav_desire != log.Desire.none and self.lane_change_state == LaneChangeState.off:
self.desire = nav_desire
if nav_desire in (log.Desire.turnLeft, log.Desire.turnRight):
self.turn_direction = nav_desire
@@ -647,6 +647,9 @@ class LatControlTorque(LatControl):
output_torque *= tucson_4th_gen_center_taper
elif genesis_g70_active:
output_torque *= genesis_g70_center_output_taper
output_torque *= get_genesis_g70_highway_turn_in_output_scale(
output_torque, setpoint, measurement, desired_lateral_jerk, CS.vEgo,
)
output_torque *= get_genesis_g70_high_speed_error_scale(
setpoint, measurement, desired_lateral_jerk, CS.vEgo,
)
@@ -276,7 +276,7 @@ GENESIS_GV70_OUTPUT_SMOOTHING_UNWIND_PHASE_WIDTH = 0.08
GENESIS_GV70_OUTPUT_SMOOTHING_DIRECTION_CHANGE_LAT = 0.55
GENESIS_GV70_OUTPUT_SMOOTHING_DIRECTION_CHANGE_RC = 0.065
GENESIS_GV70_HIGHWAY_STABILIZER_SPEED_BP = [40.0 * CV.MPH_TO_MS, 50.0 * CV.MPH_TO_MS]
GENESIS_GV70_HIGHWAY_STABILIZER_CENTER_LAT_BP = [0.45, 0.65]
GENESIS_GV70_HIGHWAY_STABILIZER_CENTER_LAT_BP = [0.75, 1.0]
GENESIS_GV70_HIGHWAY_STABILIZER_BASELINE_RC = 0.85
GENESIS_GV70_HIGHWAY_STABILIZER_BLEND_RC = 0.35
GENESIS_GV70_HIGHWAY_STABILIZER_REVERSAL_LAT = 0.06
@@ -288,6 +288,11 @@ GENESIS_G70_FRICTION_THRESHOLD_GAIN = 0.10
GENESIS_G70_CURVE_TURN_IN_JERK_REDUCTION = 0.50
GENESIS_G70_CURVE_TURN_IN_SPEED_BP = [20.0, 25.0]
GENESIS_G70_CURVE_TURN_IN_LAT_BP = [0.35, 0.70]
GENESIS_G70_HIGHWAY_TURN_IN_OUTPUT_REDUCTION = 0.12
GENESIS_G70_HIGHWAY_TURN_IN_SPEED_BP = [26.0, 32.0]
GENESIS_G70_HIGHWAY_TURN_IN_LAT_BP = [0.70, 1.10]
GENESIS_G70_HIGHWAY_TURN_IN_JERK_BP = [0.25, 0.60]
GENESIS_G70_HIGHWAY_TURN_IN_TRACKING_BP = [0.70, 0.90, 1.10]
GENESIS_G70_FRICTION_THRESHOLD_SPEED_BP = [10.0, 20.0]
GENESIS_G70_FRICTION_THRESHOLD_SPEED_V = [1.0, 2.0]
GENESIS_G70_FRICTION_SPEED_ONSET = 10.0
@@ -3506,6 +3511,20 @@ def get_genesis_g70_high_speed_error_scale(setpoint: float, measured_lateral_acc
return 1.0 - reduction
def get_genesis_g70_highway_turn_in_output_scale(output_torque: float, setpoint: float,
measured_lateral_accel: float,
desired_lateral_jerk: float, v_ego: float) -> float:
if (setpoint * desired_lateral_jerk <= 0.0 or setpoint * measured_lateral_accel <= 0.0 or
output_torque * setpoint >= 0.0):
return 1.0
speed_weight = np.interp(v_ego, GENESIS_G70_HIGHWAY_TURN_IN_SPEED_BP, [0.0, 1.0])
curve_weight = np.interp(abs(setpoint), GENESIS_G70_HIGHWAY_TURN_IN_LAT_BP, [0.0, 1.0])
jerk_weight = np.interp(abs(desired_lateral_jerk), GENESIS_G70_HIGHWAY_TURN_IN_JERK_BP, [0.0, 1.0])
tracking_weight = np.interp(abs(measured_lateral_accel / setpoint),
GENESIS_G70_HIGHWAY_TURN_IN_TRACKING_BP, [0.0, 1.0, 0.0])
return 1.0 - GENESIS_G70_HIGHWAY_TURN_IN_OUTPUT_REDUCTION * speed_weight * curve_weight * jerk_weight * tracking_weight
def get_genesis_g70_stabilized_output(output_torque: float, prev_output_torque: float,
desired_lateral_accel: float, measured_lateral_accel: float,
desired_lateral_jerk: float, v_ego: float, dt: float) -> float:
@@ -54,3 +54,23 @@ def test_full_overshoot_blend_preserves_large_error_protection():
assert tunes.get_genesis_g70_overshoot_blend(0.8, 1.0) == 1.0
assert tunes.get_genesis_g70_overshoot_blend(-0.8, -1.0) == 1.0
assert tunes.get_genesis_g70_unwind_ff_scale(0.8, 1.0, -0.5, 30) < 1.0
@pytest.mark.parametrize('direction', [-1, 1])
def test_highway_turn_in_taper_only_near_tracking_target(direction):
scale = tunes.get_genesis_g70_highway_turn_in_output_scale
args = (-direction * 0.35, direction * 1.2, direction * 1.08, direction * 0.6, 32.0)
assert scale(*args) == pytest.approx(0.88)
assert scale(*args[:-1], 20.0) == 1.0
assert scale(args[0], args[1], direction * 0.5, args[3], args[4]) == 1.0
assert scale(args[0], args[1], direction * 1.32, args[3], args[4]) == 1.0
assert scale(args[0], args[1], args[2], -args[3], args[4]) == 1.0
assert scale(-args[0], args[1], args[2], args[3], args[4]) == 1.0
@pytest.mark.parametrize('direction', [-1, 1])
def test_highway_turn_in_taper_continuous_at_tracking_boundary(direction):
scale = tunes.get_genesis_g70_highway_turn_in_output_scale
values = [scale(-direction * 0.35, direction * 1.2, direction * (1.2 + epsilon),
direction * 0.6, 32.0) for epsilon in [-1e-7, 1e-7]]
assert abs(values[1] - values[0]) < 1e-5
@@ -41,9 +41,21 @@ def test_repeated_highway_reversals_are_bounded_and_damped():
assert np.max(np.abs(np.array(raw) - np.array(shaped))) <= 0.20 + 1e-6
def test_repeated_moderate_curve_reversals_are_damped():
stabilizer = GenesisGV70HighwayCommandStabilizer()
raw, shaped = [], []
for i in range(1200):
accel = 0.55 + 0.25 * math.sin(2.0 * math.pi * 0.5 * i * 0.01)
raw.append(accel)
shaped.append(update_accel(stabilizer, accel))
assert np.std(shaped[600:]) < 0.75 * np.std(raw[600:])
assert np.max(np.abs(np.array(raw) - np.array(shaped))) <= 0.20 + 1e-6
def test_sustained_curve_is_unchanged():
stabilizer = GenesisGV70HighwayCommandStabilizer()
curve = np.concatenate((np.linspace(0.0, 0.55, 150), np.full(300, 0.55), np.linspace(0.55, 0.0, 150)))
curve = np.concatenate((np.linspace(0.0, 0.8, 150), np.full(300, 0.8), np.linspace(0.8, 0.0, 150)))
for accel in curve:
assert update_accel(stabilizer, float(accel)) == pytest.approx(accel)
@@ -55,7 +67,7 @@ def test_strong_turn_and_driver_input_reset_stabilizer():
update_accel(stabilizer, accel)
for _ in range(100):
assert update_accel(stabilizer, 0.8) == pytest.approx(0.8)
assert update_accel(stabilizer, 1.2) == pytest.approx(1.2)
for _ in range(100):
assert update_accel(stabilizer, 0.4) == pytest.approx(0.4)
@@ -1,4 +1,5 @@
from types import SimpleNamespace
from time import monotonic
from cereal import log
@@ -51,7 +52,7 @@ def test_nav_desires_keep_left_when_route_requests_it():
helper = DesireHelper()
helper.nav_desires_allowed = True
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {"valid": True, "maneuverModifier": "slightLeft"}
helper._nav_instruction_state = {"valid": True, "maneuverModifier": "slightLeft", "updatedAtMonotonic": monotonic()}
helper.update(
make_car_state(vEgo=20.0, steeringPressed=True, steeringTorque=1.0),
@@ -68,7 +69,10 @@ def test_nav_desires_turn_right_below_lane_change_speed():
helper = DesireHelper()
helper.nav_desires_allowed = True
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {"valid": True, "maneuverType": "turn", "maneuverModifier": "right", "maneuverDistance": 10.0}
helper._nav_instruction_state = {
"valid": True, "maneuverType": "turn", "maneuverModifier": "right",
"maneuverDistance": 10.0, "updatedAtMonotonic": monotonic(),
}
helper.update(
make_car_state(vEgo=5.0, rightBlinker=True),
@@ -84,7 +88,10 @@ def test_nav_desires_turn_right_below_lane_change_speed():
def test_nav_desires_turn_preview_starts_before_last_second():
helper = DesireHelper()
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {"valid": True, "maneuverType": "turn", "maneuverModifier": "right", "maneuverDistance": 50.0}
helper._nav_instruction_state = {
"valid": True, "maneuverType": "turn", "maneuverModifier": "right",
"maneuverDistance": 50.0, "updatedAtMonotonic": monotonic(),
}
helper.update(
make_car_state(vEgo=10.5, rightBlinker=True),
@@ -98,17 +105,73 @@ def test_nav_desires_turn_preview_starts_before_last_second():
assert helper.lane_change_state == LaneChangeState.off
def test_routed_turn_replays_signaled_approach_independently_of_lane_change_setting():
samples = (
(82.6, 13.64, True, log.Desire.none),
(69.1, 13.15, True, log.Desire.turnRight),
(56.3, 13.18, True, log.Desire.turnRight),
(43.1, 13.00, False, log.Desire.none),
)
for lane_change_speed in (2.777777, 11.1):
helper = DesireHelper()
helper._update_nav_params = lambda: None
toggles = make_toggles(minimum_lane_change_speed=lane_change_speed)
for distance, speed, right_blinker, expected in samples:
helper._nav_instruction_state = {
"valid": True,
"maneuverType": "turn",
"maneuverModifier": "right",
"maneuverDistance": distance,
"updatedAtMonotonic": monotonic(),
}
helper.update(
make_car_state(vEgo=speed, rightBlinker=right_blinker),
True,
0.0,
make_plan(),
toggles,
)
assert helper.desire == expected
assert helper.lane_change_state == LaneChangeState.off
assert helper.turn_direction == expected
def test_stale_route_instruction_cannot_request_turn_or_suppress_lane_change():
helper = DesireHelper()
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {
"valid": True,
"maneuverType": "turn",
"maneuverModifier": "right",
"maneuverDistance": 55.0,
"updatedAtMonotonic": monotonic() - 10.0,
}
helper.update(
make_car_state(vEgo=13.0, rightBlinker=True),
True,
0.0,
make_plan(),
make_toggles(minimum_lane_change_speed=2.777777),
)
assert helper.desire == log.Desire.none
assert helper.lane_change_state == LaneChangeState.preLaneChange
def test_nav_desires_turn_preview_is_bounded_and_requires_matching_signal():
for distance, blinker, speed, maneuver_type in (
(65.0, True, 10.5, "turn"),
(-1.0, True, 10.5, "turn"),
(50.0, False, 10.5, "turn"),
(50.0, True, 11.2, "turn"),
(50.0, True, 14.2, "turn"),
(50.0, True, 10.5, "arrive"),
):
helper = DesireHelper()
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {"valid": True, "maneuverType": maneuver_type, "maneuverModifier": "right", "maneuverDistance": distance}
helper._nav_instruction_state = {
"valid": True, "maneuverType": maneuver_type, "maneuverModifier": "right",
"maneuverDistance": distance, "updatedAtMonotonic": monotonic(),
}
helper.update(
make_car_state(vEgo=speed, rightBlinker=blinker),
@@ -124,7 +187,10 @@ def test_nav_desires_turn_preview_is_bounded_and_requires_matching_signal():
def test_nav_desires_turn_preview_respects_stop_hold():
helper = DesireHelper()
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {"valid": True, "maneuverType": "turn", "maneuverModifier": "right", "maneuverDistance": 20.0}
helper._nav_instruction_state = {
"valid": True, "maneuverType": "turn", "maneuverModifier": "right",
"maneuverDistance": 20.0, "updatedAtMonotonic": monotonic(),
}
helper.update(
make_car_state(vEgo=5.0, rightBlinker=True),
@@ -143,7 +209,10 @@ def test_nav_desires_turn_requires_matching_blinker():
helper = DesireHelper()
helper.nav_desires_allowed = True
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {"valid": True, "maneuverType": "turn", "maneuverModifier": modifier, "maneuverDistance": 10.0}
helper._nav_instruction_state = {
"valid": True, "maneuverType": "turn", "maneuverModifier": modifier,
"maneuverDistance": 10.0, "updatedAtMonotonic": monotonic(),
}
helper.update(
make_car_state(vEgo=5.0, **{opposite_blinker: True}),
@@ -160,7 +229,10 @@ def test_nav_desires_turn_right_waits_until_turn_is_close():
helper = DesireHelper()
helper.nav_desires_allowed = True
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {"valid": True, "maneuverType": "turn", "maneuverModifier": "right", "maneuverDistance": 300.0}
helper._nav_instruction_state = {
"valid": True, "maneuverType": "turn", "maneuverModifier": "right",
"maneuverDistance": 300.0, "updatedAtMonotonic": monotonic(),
}
helper.update(
make_car_state(vEgo=5.0),
@@ -178,6 +250,7 @@ def test_matching_routed_turn_does_not_start_lane_change_above_threshold():
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {
"valid": True,
"updatedAtMonotonic": monotonic(),
"maneuverType": "turn",
"maneuverModifier": "right",
"maneuverDistance": 111.0,
@@ -201,6 +274,7 @@ def test_distant_routed_turn_does_not_block_lane_change():
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {
"valid": True,
"updatedAtMonotonic": monotonic(),
"maneuverType": "turn",
"maneuverModifier": "right",
"maneuverDistance": 794.0,
@@ -223,6 +297,7 @@ def test_matching_routed_turn_cancels_pending_lane_change_before_it_starts():
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {
"valid": True,
"updatedAtMonotonic": monotonic(),
"maneuverType": "turn",
"maneuverModifier": "left",
"maneuverDistance": 125.0,
@@ -249,6 +324,7 @@ def test_nav_desires_off_ramp_lane_guidance_becomes_keep_right():
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {
"valid": True,
"updatedAtMonotonic": monotonic(),
"maneuverType": "off ramp",
"maneuverModifier": "right",
"activeLaneDirection": "slightRight",
@@ -272,6 +348,7 @@ def test_nav_desires_off_ramp_lane_guidance_waits_until_split_is_close():
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {
"valid": True,
"updatedAtMonotonic": monotonic(),
"maneuverType": "off ramp",
"maneuverModifier": "right",
"activeLaneDirection": "slightRight",
@@ -295,6 +372,7 @@ def test_nav_desires_ambiguous_off_ramp_waits_longer_before_keep_right():
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {
"valid": True,
"updatedAtMonotonic": monotonic(),
"maneuverType": "off ramp",
"maneuverModifier": "right",
"activeLaneDirection": "slightRight",
@@ -319,6 +397,7 @@ def test_nav_desires_edge_exit_lane_with_shared_transition_lane_does_not_keep_ri
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {
"valid": True,
"updatedAtMonotonic": monotonic(),
"maneuverType": "off ramp",
"maneuverModifier": "right",
"activeLaneDirection": "slightRight",
@@ -346,6 +425,7 @@ def test_nav_desires_wide_highway_edge_exit_lane_keeps_right():
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {
"valid": True,
"updatedAtMonotonic": monotonic(),
"maneuverType": "off ramp",
"maneuverModifier": "right",
"activeLaneDirection": "slightRight",
@@ -373,6 +453,7 @@ def test_nav_desires_shared_transition_lane_keeps_when_active_lane_is_not_outerm
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {
"valid": True,
"updatedAtMonotonic": monotonic(),
"maneuverType": "off ramp",
"maneuverModifier": "right",
"activeLaneDirection": "slightRight",
@@ -399,6 +480,7 @@ def test_nav_desires_ambiguous_fork_slight_right_only_keeps_close_to_split():
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {
"valid": True,
"updatedAtMonotonic": monotonic(),
"maneuverType": "fork",
"maneuverModifier": "slightRight",
"activeLaneDirection": "slightRight",
@@ -423,6 +505,7 @@ def test_nav_desires_ambiguous_fork_slight_right_does_not_nudge_too_early():
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {
"valid": True,
"updatedAtMonotonic": monotonic(),
"maneuverType": "fork",
"maneuverModifier": "slightRight",
"activeLaneDirection": "slightRight",
@@ -447,6 +530,7 @@ def test_nav_desires_fork_with_active_straight_lane_does_not_turn_left():
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {
"valid": True,
"updatedAtMonotonic": monotonic(),
"maneuverType": "fork",
"maneuverModifier": "left",
"activeLaneDirection": "straight",
@@ -468,7 +552,7 @@ def test_nav_desires_do_not_override_lane_change_state_machine():
helper = DesireHelper()
helper.nav_desires_allowed = True
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {"valid": True, "maneuverModifier": "slightRight"}
helper._nav_instruction_state = {"valid": True, "maneuverModifier": "slightRight", "updatedAtMonotonic": monotonic()}
helper.lane_change_state = LaneChangeState.laneChangeStarting
helper.lane_change_direction = LaneChangeDirection.left
helper.lane_change_ll_prob = 0.5
@@ -581,7 +665,7 @@ def test_nav_desires_nudgeless_only_when_engaged_blocks_keep_when_aol_only():
helper = DesireHelper()
helper.nav_desires_allowed = True
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {"valid": True, "maneuverModifier": "slightLeft"}
helper._nav_instruction_state = {"valid": True, "maneuverModifier": "slightLeft", "updatedAtMonotonic": monotonic()}
helper.update(
make_car_state(vEgo=20.0),
@@ -651,7 +735,7 @@ def test_turn_desire_released_after_stop_completes():
def test_nav_desires_disabled_leave_desire_unchanged():
helper = DesireHelper()
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {"valid": True, "maneuverModifier": "left"}
helper._nav_instruction_state = {"valid": True, "maneuverModifier": "left", "updatedAtMonotonic": monotonic()}
helper.update(
make_car_state(vEgo=5.0),
@@ -667,7 +751,7 @@ def test_nav_desires_disabled_leave_desire_unchanged():
def test_disabling_nav_desires_clears_active_route_desire_immediately():
helper = DesireHelper()
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {"valid": True, "maneuverModifier": "slightRight"}
helper._nav_instruction_state = {"valid": True, "maneuverModifier": "slightRight", "updatedAtMonotonic": monotonic()}
car_state = make_car_state(vEgo=20.0, steeringPressed=True, steeringTorque=-1.0)
plan = make_plan(laneWidthRight=4.2)
@@ -681,7 +765,7 @@ def test_disabling_nav_desires_clears_active_route_desire_immediately():
def test_nav_lane_positioning_requires_driver_confirmation():
helper = DesireHelper()
helper._update_nav_params = lambda: None
helper._nav_instruction_state = {"valid": True, "maneuverModifier": "slightRight"}
helper._nav_instruction_state = {"valid": True, "maneuverModifier": "slightRight", "updatedAtMonotonic": monotonic()}
helper.update(
make_car_state(vEgo=20.0),
+66 -15
View File
@@ -33,6 +33,18 @@ VOLUME_BASE = 20
if HARDWARE.get_device_type() in ("tici", "tizi"):
VOLUME_BASE = 10
VOLUME_SETTINGS_REFRESH_INTERVAL = 0.25
VOLUME_SETTING_KEYS = (
"BelowSteerSpeedVolume",
"DisengageVolume",
"EngageVolume",
"PromptVolume",
"PromptDistractedVolume",
"RefuseVolume",
"WarningImmediateVolume",
"WarningSoftVolume",
)
AudibleAlert = log.SelfdriveState.AudibleAlert
StarPilotAudibleAlert = custom.StarPilotCarControl.HUDControl.AudibleAlert
@@ -68,6 +80,23 @@ def should_mute_turn_steering_limit_alert(alert_type: str, v_ego: float, mute_be
)
def read_volume_settings(params):
def read_int(key, minimum=0):
try:
value = int(params.get_int(key, return_default=True, default=101))
except (TypeError, ValueError, OSError):
value = 101
return max(minimum, min(value, 101))
return {
"alert_volume_controller": params.get_bool("AlertVolumeControl"),
**{
key: read_int(key, minimum=25 if key in ("WarningImmediateVolume", "WarningSoftVolume") else 0)
for key in VOLUME_SETTING_KEYS
},
}
sound_list: dict[int, tuple[str, int | None, float]] = {
GPU_MODEL_READY_ALERT: ("model_ready.wav", 1, MAX_VOLUME),
# AudibleAlert, file name, play count (none for infinite)
@@ -127,6 +156,9 @@ class Soundd:
self.spl_filter_weighted = FirstOrderFilter(0, 2.5, FILTER_DT, initialized=False)
self.params_memory = Params(memory=True)
self.volume_params = Params(return_defaults=True)
self.volume_settings = read_volume_settings(self.volume_params)
self.last_volume_settings_refresh = 0.0
from openpilot.starpilot.common.gpu_model_ready_sound import GpuModelReadyChime
self.model_ready_chime = GpuModelReadyChime()
self.model_ready_params = Params(return_defaults=True)
@@ -362,7 +394,7 @@ class Soundd:
def get_volume_override(self):
if self.current_alert_type.startswith("belowSteerSpeed/"):
return self.starpilot_toggles.below_steer_speed_volume / 100.0
return self.volume_settings["BelowSteerSpeedVolume"] / 100.0
return self.volume_map.get(self.current_alert, 1.01)
@@ -410,6 +442,7 @@ class Soundd:
while True:
sm.update(0)
self.refresh_volume_settings()
self.update_bluetooth_audio()
if self.pending_stream_status is not None:
@@ -422,7 +455,7 @@ class Soundd:
self.auto_volume = self.calculate_volume(float(self.spl_filter_weighted.x))
self.current_volume = self.auto_volume
if self.starpilot_toggles.alert_volume_controller:
if self.volume_settings["alert_volume_controller"]:
self.current_volume = 0.0
self.get_audible_alert(sm)
@@ -436,7 +469,7 @@ class Soundd:
float(getattr(self.starpilot_toggles, "turn_steering_limit_mute_speed", 0.0)),
):
self.current_volume = 0.0
elif self.starpilot_toggles.alert_volume_controller:
elif self.volume_settings["alert_volume_controller"]:
self.current_volume = self.get_volume_override()
if self.current_volume == 1.01:
self.current_volume = self.auto_volume
@@ -465,25 +498,43 @@ class Soundd:
except Exception:
cloudlog.exception("soundd: failed to close stream")
def update_starpilot_sounds(self, sd=None, stream=None):
def refresh_volume_settings(self, now=None):
now = time.monotonic() if now is None else now
if now - self.last_volume_settings_refresh < VOLUME_SETTINGS_REFRESH_INTERVAL:
return False
self.last_volume_settings_refresh = now
volume_settings = read_volume_settings(self.volume_params)
if volume_settings == self.volume_settings:
return False
self.volume_settings = volume_settings
self.update_volume_map()
return True
def update_volume_map(self):
settings = self.volume_settings
self.volume_map = {
AudibleAlert.engage: self.starpilot_toggles.engage_volume / 100.0,
AudibleAlert.disengage: self.starpilot_toggles.disengage_volume / 100.0,
AudibleAlert.refuse: self.starpilot_toggles.refuse_volume / 100.0,
AudibleAlert.engage: settings["EngageVolume"] / 100.0,
AudibleAlert.disengage: settings["DisengageVolume"] / 100.0,
AudibleAlert.refuse: settings["RefuseVolume"] / 100.0,
AudibleAlert.prompt: self.starpilot_toggles.prompt_volume / 100.0,
AudibleAlert.promptRepeat: self.starpilot_toggles.prompt_volume / 100.0,
AudibleAlert.promptDistracted: self.starpilot_toggles.promptDistracted_volume / 100.0,
AudibleAlert.prompt: settings["PromptVolume"] / 100.0,
AudibleAlert.promptRepeat: settings["PromptVolume"] / 100.0,
AudibleAlert.promptDistracted: settings["PromptDistractedVolume"] / 100.0,
AudibleAlert.preAlert: self.starpilot_toggles.promptDistracted_volume / 100.0,
AudibleAlert.preAlert: settings["PromptDistractedVolume"] / 100.0,
AudibleAlert.warningSoft: self.starpilot_toggles.warningSoft_volume / 100.0,
AudibleAlert.warningImmediate: self.starpilot_toggles.warningImmediate_volume / 100.0,
AudibleAlert.warningSoft: settings["WarningSoftVolume"] / 100.0,
AudibleAlert.warningImmediate: settings["WarningImmediateVolume"] / 100.0,
starpilot_alert_key(StarPilotAudibleAlert.goat): self.starpilot_toggles.prompt_volume / 100.0,
starpilot_alert_key(StarPilotAudibleAlert.startup): self.starpilot_toggles.engage_volume / 100.0
starpilot_alert_key(StarPilotAudibleAlert.goat): settings["PromptVolume"] / 100.0,
starpilot_alert_key(StarPilotAudibleAlert.startup): settings["EngageVolume"] / 100.0
}
def update_starpilot_sounds(self, sd=None, stream=None):
self.update_volume_map()
for sound in sound_list:
if sound not in self.volume_map:
self.volume_map[sound] = 1.01
+34
View File
@@ -7,6 +7,7 @@ from openpilot.selfdrive.ui.soundd import (
Soundd,
check_selfdrive_timeout_alert,
is_turn_steering_limit_alert,
read_volume_settings,
should_mute_turn_steering_limit_alert,
starpilot_alert_key,
)
@@ -20,6 +21,39 @@ StarPilotAudibleAlert = custom.StarPilotCarControl.HUDControl.AudibleAlert
class TestSoundd:
def test_volume_settings_are_read_as_percentages(self):
class FakeParams:
values = {
"AlertVolumeControl": True,
"BelowSteerSpeedVolume": 0,
"PromptVolume": 20,
"WarningSoftVolume": 25,
"WarningImmediateVolume": 101,
}
def get_bool(self, key):
return self.values.get(key, False)
def get_int(self, key, return_default=False, default=101):
return self.values.get(key, default)
params = FakeParams()
settings = read_volume_settings(params)
assert settings["alert_volume_controller"] is True
assert settings["BelowSteerSpeedVolume"] == 0
assert settings["PromptVolume"] == 20
assert settings["WarningImmediateVolume"] == 101
soundd = Soundd.__new__(Soundd)
soundd.volume_params = params
soundd.volume_settings = settings
soundd.last_volume_settings_refresh = 0.0
soundd.update_volume_map()
params.values["PromptVolume"] = 35
assert soundd.refresh_volume_settings(now=1.0)
assert soundd.volume_map[AudibleAlert.promptRepeat] == 0.35
def test_does_not_consume_car_state_reader(self):
assert "carState" not in SOUNDD_SERVICES
assert "starpilotSelfdriveState" in SOUNDD_SERVICES