mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-30 11:23:49 +08:00
Desires
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user