mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-10-21 07:13:42 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44c18879fd | |||
| d78c25df81 | |||
| 1b4902a8ba | |||
| fb95b72e23 | |||
| 895bfa8ea5 | |||
| 45051dab54 | |||
| 612c79760a | |||
| af93efd608 | |||
| 628bda0bbc | |||
| 05493840f3 |
@@ -4,6 +4,7 @@
|
||||
[submodule "opendbc"]
|
||||
path = opendbc_repo
|
||||
url = https://github.com/sunnypilot/opendbc.git
|
||||
branch = toyotabsmhack
|
||||
[submodule "msgq"]
|
||||
path = msgq_repo
|
||||
url = https://github.com/sunnypilot/msgq.git
|
||||
|
||||
+1
-1
Submodule opendbc_repo updated: 5be7507143...afc409a445
@@ -466,9 +466,6 @@ struct ModelDataV2SP @0xa1680744031fdb2d {
|
||||
leftLaneChangeEdgeBlock @1 :Bool;
|
||||
rightLaneChangeEdgeBlock @2 :Bool;
|
||||
lanefulCurvature @3 :Float32;
|
||||
lanefulTrust @4 :Float32;
|
||||
modelCurvature @5 :Float32;
|
||||
lanefulGoingAround @6 :Bool;
|
||||
|
||||
enum TurnDirection {
|
||||
none @0;
|
||||
|
||||
@@ -94,7 +94,7 @@ _services: dict[str, tuple] = {
|
||||
"carControlSP": (True, 100., 10),
|
||||
"carStateSP": (True, 100., 10),
|
||||
"liveMapDataSP": (True, 1., 1),
|
||||
"modelDataV2SP": (True, 20., 10, QueueSize.BIG),
|
||||
"modelDataV2SP": (True, 20., None, QueueSize.BIG),
|
||||
"liveLocationKalman": (True, 20.),
|
||||
|
||||
# debug
|
||||
|
||||
@@ -170,6 +170,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}},
|
||||
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
|
||||
{"FollowDistanceAccelBoost", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}},
|
||||
{"HasAcceptedTermsSP", {PERSISTENT, STRING, "0"}},
|
||||
@@ -246,6 +247,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"TeslaMadsScreenButton", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaEnhancedBsm", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
|
||||
@@ -9,6 +9,11 @@ CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N]
|
||||
|
||||
LongCtrlState = car.CarControl.Actuators.LongControlState
|
||||
|
||||
# Stopping is shaped as a constant-jerk taper so the brake is released as the car comes to rest.
|
||||
# The old behavior ramped towards CP.stopAccel while still rolling, making the last meter the hardest.
|
||||
STOPPING_JERK = 2.0 # m/s^3, rate the stopping decel is released at (higher eases off faster)
|
||||
STOPPING_HOLD_SPEED = 0.02 # m/s, below this command CP.stopAccel to hold the car
|
||||
|
||||
|
||||
def long_control_state_trans(CP_SP, active, long_control_state,
|
||||
should_stop, brake_pressed, cruise_standstill):
|
||||
@@ -51,6 +56,18 @@ class LongControl:
|
||||
def reset(self):
|
||||
self.pid.reset()
|
||||
|
||||
def _stopping_accel(self, v_ego: float) -> float:
|
||||
v_ego = max(float(v_ego), 0.0)
|
||||
if v_ego < STOPPING_HOLD_SPEED:
|
||||
return float(self.CP.stopAccel)
|
||||
|
||||
# Decel that reaches zero at standstill with constant jerk, never stronger than what we already have
|
||||
taper = -np.sqrt(2.0 * STOPPING_JERK * v_ego)
|
||||
goal = min(0.0, max(self.last_output_accel, taper))
|
||||
output_accel = np.clip(goal, self.last_output_accel - STOPPING_JERK * DT_CTRL,
|
||||
self.last_output_accel + STOPPING_JERK * DT_CTRL)
|
||||
return float(min(output_accel, 0.0))
|
||||
|
||||
def update(self, active, CS, a_target, should_stop, accel_limits):
|
||||
"""Update longitudinal control. This updates the state machine and runs a PID loop"""
|
||||
self.pid.neg_limit = accel_limits[0]
|
||||
@@ -64,11 +81,7 @@ class LongControl:
|
||||
output_accel = 0.
|
||||
|
||||
elif self.long_control_state == LongCtrlState.stopping:
|
||||
output_accel = self.last_output_accel
|
||||
if output_accel > self.CP.stopAccel:
|
||||
output_accel = min(output_accel, 0.0)
|
||||
# TODO: can we just go straight to stopAccel?
|
||||
output_accel -= 1.0 * DT_CTRL # m/s^2/s while trying to stop
|
||||
output_accel = self._stopping_accel(CS.vEgo)
|
||||
self.reset()
|
||||
|
||||
else: # LongCtrlState.pid
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from openpilot.cereal import log
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.selfdrive.controls.lib.accel_boost import (
|
||||
AccelBoost, ACCEL_BOOST_MAX, ACCEL_BOOST_PER_OVERRIDE, ACCEL_BOOST_RATE
|
||||
)
|
||||
|
||||
E2E_ACCEL = 0.5
|
||||
PLANNER_ACCEL = 2.0
|
||||
RATE_PER_STEP = ACCEL_BOOST_RATE * DT_MDL
|
||||
|
||||
|
||||
class FakeSubMaster:
|
||||
def __init__(self, enabled=True, gas_pressed=False, v_ego=15.0,
|
||||
personality=log.LongitudinalPersonality.standard):
|
||||
self._messages = {
|
||||
'selfdriveState': SimpleNamespace(enabled=enabled, personality=personality),
|
||||
'carState': SimpleNamespace(gasPressed=gas_pressed, vEgo=v_ego),
|
||||
}
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._messages[key]
|
||||
|
||||
|
||||
def run_boost(boost, sm, steps=1):
|
||||
out = E2E_ACCEL
|
||||
for _ in range(steps):
|
||||
out = boost.update(sm, E2E_ACCEL, PLANNER_ACCEL, PLANNER_ACCEL)
|
||||
return out
|
||||
|
||||
|
||||
class TestAccelBoost(OpenpilotTestCase):
|
||||
def _boost_to_max(self):
|
||||
boost = AccelBoost()
|
||||
sm = FakeSubMaster()
|
||||
steps = round(ACCEL_BOOST_PER_OVERRIDE / RATE_PER_STEP)
|
||||
# Eligibility is latched on a frame without gas, then applied on the next override.
|
||||
run_boost(boost, sm)
|
||||
for _ in range(10):
|
||||
sm['carState'].gasPressed = True
|
||||
run_boost(boost, sm, steps)
|
||||
sm['carState'].gasPressed = False
|
||||
run_boost(boost, sm)
|
||||
return boost
|
||||
|
||||
def test_no_boost_without_gas(self):
|
||||
boost = AccelBoost()
|
||||
sm = FakeSubMaster(gas_pressed=False)
|
||||
out = run_boost(boost, sm)
|
||||
self.assertEqual(boost.total_boost, 0.0)
|
||||
self.assertAlmostEqual(out, E2E_ACCEL)
|
||||
|
||||
def test_boost_requires_e2e_to_be_limiting(self):
|
||||
boost = AccelBoost()
|
||||
sm = FakeSubMaster(gas_pressed=False)
|
||||
# Eligibility is latched before the override, so keep the e2e plan at the planner limit here.
|
||||
boost.update(sm, E2E_ACCEL, E2E_ACCEL, E2E_ACCEL)
|
||||
sm['carState'].gasPressed = True
|
||||
for _ in range(10):
|
||||
boost.update(sm, E2E_ACCEL, E2E_ACCEL, E2E_ACCEL)
|
||||
self.assertEqual(boost.total_boost, 0.0)
|
||||
|
||||
def test_per_override_cap(self):
|
||||
boost = AccelBoost()
|
||||
sm = FakeSubMaster(gas_pressed=False)
|
||||
run_boost(boost, sm)
|
||||
sm['carState'].gasPressed = True
|
||||
run_boost(boost, sm, 1000)
|
||||
self.assertAlmostEqual(boost.total_boost, ACCEL_BOOST_PER_OVERRIDE)
|
||||
|
||||
def test_max_boost(self):
|
||||
boost = self._boost_to_max()
|
||||
self.assertAlmostEqual(boost.total_boost, ACCEL_BOOST_MAX)
|
||||
|
||||
def test_boost_is_applied_to_e2e_accel(self):
|
||||
boost = self._boost_to_max()
|
||||
sm = FakeSubMaster(gas_pressed=True)
|
||||
out = run_boost(boost, sm)
|
||||
self.assertAlmostEqual(out, E2E_ACCEL + boost.total_boost)
|
||||
|
||||
def test_boost_does_not_depend_on_following_distance(self):
|
||||
totals = []
|
||||
for personality in (log.LongitudinalPersonality.relaxed,
|
||||
log.LongitudinalPersonality.standard,
|
||||
log.LongitudinalPersonality.aggressive):
|
||||
boost = AccelBoost()
|
||||
sm = FakeSubMaster(personality=personality)
|
||||
steps = round(ACCEL_BOOST_PER_OVERRIDE / RATE_PER_STEP)
|
||||
run_boost(boost, sm)
|
||||
for _ in range(10):
|
||||
sm['carState'].gasPressed = True
|
||||
run_boost(boost, sm, steps)
|
||||
sm['carState'].gasPressed = False
|
||||
run_boost(boost, sm)
|
||||
totals.append(boost.total_boost)
|
||||
|
||||
self.assertEqual(len(totals), 3)
|
||||
for total in totals:
|
||||
self.assertAlmostEqual(total, ACCEL_BOOST_MAX)
|
||||
@@ -1,6 +1,10 @@
|
||||
from opendbc.car.structs import car
|
||||
|
||||
from openpilot.common.realtime import DT_CTRL
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.cereal import custom
|
||||
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState, long_control_state_trans
|
||||
from openpilot.selfdrive.controls.lib.longcontrol import (LongControl, LongCtrlState, STOPPING_HOLD_SPEED,
|
||||
STOPPING_JERK, long_control_state_trans)
|
||||
|
||||
|
||||
class TestLongControlStateTransition(OpenpilotTestCase):
|
||||
@@ -42,3 +46,75 @@ class TestLongControlStateTransition(OpenpilotTestCase):
|
||||
next_state = long_control_state_trans(CP_SP, active, current_state,
|
||||
should_stop=False, brake_pressed=False, cruise_standstill=False)
|
||||
assert next_state == LongCtrlState.pid
|
||||
|
||||
|
||||
class TestLongControlStopping(OpenpilotTestCase):
|
||||
def _long_control(self, stop_accel=-2.0, entry_accel=-1.2, v_ego=0.3):
|
||||
CP = car.CarParams.new_message()
|
||||
CP.longitudinalTuning.kiBP = [0.0]
|
||||
CP.longitudinalTuning.kiV = [0.0]
|
||||
CP.stopAccel = stop_accel
|
||||
lc = LongControl(CP, custom.CarParamsSP.new_message())
|
||||
lc.long_control_state = LongCtrlState.stopping
|
||||
lc.last_output_accel = entry_accel
|
||||
|
||||
CS = car.CarState.new_message()
|
||||
CS.vEgo = float(v_ego)
|
||||
CS.brakePressed = False
|
||||
CS.cruiseState.standstill = False
|
||||
return lc, CS
|
||||
|
||||
def _run_stop(self, lc, CS, frames=500):
|
||||
accels = []
|
||||
distance = 0.0
|
||||
for _ in range(frames):
|
||||
accels.append(lc.update(True, CS, 0.0, True, (-4.0, 2.0)))
|
||||
if CS.vEgo < STOPPING_HOLD_SPEED:
|
||||
break
|
||||
distance += CS.vEgo * DT_CTRL
|
||||
CS.vEgo = float(max(0.0, CS.vEgo + accels[-1] * DT_CTRL))
|
||||
return accels, distance
|
||||
|
||||
def test_releases_braking_instead_of_ramping_to_stop_accel(self):
|
||||
lc, CS = self._long_control(entry_accel=-1.2)
|
||||
accels, _ = self._run_stop(lc, CS)
|
||||
# the stop state must ease off the brake, not add more of it as the car slows
|
||||
assert accels[0] > -1.2
|
||||
assert accels[-2] > accels[0]
|
||||
# and it must actually get gentle before standstill instead of stopping at the entry decel
|
||||
assert accels[-2] > -0.8
|
||||
|
||||
def test_never_increases_decel_while_rolling(self):
|
||||
lc, CS = self._long_control(entry_accel=-1.2)
|
||||
accels, _ = self._run_stop(lc, CS)
|
||||
rolling = accels[:-1]
|
||||
assert all(b >= a - 1e-9 for a, b in zip(rolling[:-1], rolling[1:], strict=False)), rolling
|
||||
|
||||
def test_jerk_is_bounded(self):
|
||||
lc, CS = self._long_control(entry_accel=-1.5)
|
||||
accels, _ = self._run_stop(lc, CS)
|
||||
rolling = accels[:-1]
|
||||
max_step = max(abs(b - a) for a, b in zip(rolling[:-1], rolling[1:], strict=False))
|
||||
assert max_step <= STOPPING_JERK * DT_CTRL + 1e-9
|
||||
|
||||
def test_holds_with_stop_accel_at_standstill(self):
|
||||
lc, CS = self._long_control(stop_accel=-2.0)
|
||||
accels, _ = self._run_stop(lc, CS)
|
||||
assert CS.vEgo < STOPPING_HOLD_SPEED
|
||||
assert accels[-1] == -2.0
|
||||
|
||||
def test_weak_entry_does_not_add_braking(self):
|
||||
lc, CS = self._long_control(entry_accel=-0.2)
|
||||
accels, _ = self._run_stop(lc, CS)
|
||||
assert min(accels[:-1]) >= -0.2 - 1e-9
|
||||
|
||||
def test_never_commands_positive_accel(self):
|
||||
lc, CS = self._long_control(entry_accel=0.5)
|
||||
accels, _ = self._run_stop(lc, CS)
|
||||
assert max(accels) <= 0.0
|
||||
|
||||
def test_stops_within_short_distance(self):
|
||||
lc, CS = self._long_control(entry_accel=-1.2)
|
||||
_, distance = self._run_stop(lc, CS)
|
||||
assert CS.vEgo < STOPPING_HOLD_SPEED
|
||||
assert distance < 0.25
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_boost_ext import get_accel_boost_max
|
||||
|
||||
BAR_WIDTH = 10
|
||||
VALUE_FONT_SIZE = 17
|
||||
LABEL_FONT_SIZE = 10
|
||||
|
||||
COLOR_TRACK = rl.Color(255, 255, 255, 40)
|
||||
COLOR_TEXT = rl.Color(255, 255, 255, 235)
|
||||
|
||||
PROFILE_STYLES = {
|
||||
0: ("AGGR", rl.Color(255, 80, 60, 255)),
|
||||
1: ("STD", rl.Color(0, 230, 120, 255)),
|
||||
2: ("RELAX", rl.Color(70, 150, 255, 255)),
|
||||
}
|
||||
|
||||
|
||||
class AccelBoostIndicator(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._boost_filter = FirstOrderFilter(0.0, 0.15, 1 / gui_app.target_fps)
|
||||
self._alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
|
||||
self._font = gui_app.font(FontWeight.BOLD)
|
||||
self._label_font = gui_app.font(FontWeight.MEDIUM)
|
||||
self._profile_label = "STD"
|
||||
self._profile_color = PROFILE_STYLES[1][1]
|
||||
self._max_boost = 0.0
|
||||
|
||||
def _update_state(self):
|
||||
sm = ui_state.sm
|
||||
visible = (ui_state.has_longitudinal_control and sm.alive['selfdriveState'] and sm.alive['longitudinalPlan'] and
|
||||
sm['selfdriveState'].enabled)
|
||||
self._alpha_filter.update(1.0 if visible else 0.0)
|
||||
self._boost_filter.update(float(sm['longitudinalPlan'].accelBoost) if visible else 0.0)
|
||||
|
||||
if sm.alive['selfdriveState']:
|
||||
personality = sm['selfdriveState'].personality
|
||||
self._profile_label, self._profile_color = PROFILE_STYLES.get(personality.raw, PROFILE_STYLES[1])
|
||||
self._max_boost = get_accel_boost_max(personality)
|
||||
|
||||
def _render(self, _):
|
||||
alpha = self._alpha_filter.x
|
||||
if alpha < 0.01:
|
||||
return
|
||||
|
||||
panel_x = self.rect.x + self.rect.width - SIDE_PANEL_WIDTH
|
||||
margin = min(120.0, max(56.0, self.rect.height * 0.14))
|
||||
track_top = self.rect.y + margin
|
||||
track_bottom = self.rect.y + self.rect.height - margin
|
||||
track_h = max(1.0, track_bottom - track_top)
|
||||
cx = panel_x + SIDE_PANEL_WIDTH / 2
|
||||
bar_x = cx - BAR_WIDTH / 2
|
||||
|
||||
# track
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(bar_x, track_top, BAR_WIDTH, track_h), 0.5, 12,
|
||||
rl.Color(COLOR_TRACK.r, COLOR_TRACK.g, COLOR_TRACK.b, int(COLOR_TRACK.a * alpha)))
|
||||
|
||||
if self._max_boost <= 0.0:
|
||||
return
|
||||
|
||||
# Scale to the current profile's cap, so a maxed boost fills the bar.
|
||||
boost = min(max(self._boost_filter.x, 0.0), self._max_boost)
|
||||
fill_h = boost / self._max_boost * track_h
|
||||
if fill_h > 0.0:
|
||||
fill = rl.Color(self._profile_color.r, self._profile_color.g, self._profile_color.b, int(255 * alpha))
|
||||
if fill_h < BAR_WIDTH:
|
||||
rl.draw_circle(int(cx), int(track_bottom - fill_h / 2), max(1, int(BAR_WIDTH / 2)), fill)
|
||||
else:
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(bar_x, track_bottom - fill_h, BAR_WIDTH, fill_h), 0.5, 12, fill)
|
||||
|
||||
label_size = measure_text_cached(self._label_font, self._profile_label, LABEL_FONT_SIZE)
|
||||
rl.draw_text_ex(self._label_font, self._profile_label, rl.Vector2(cx - label_size.x / 2, track_top - 42),
|
||||
LABEL_FONT_SIZE, 0,
|
||||
rl.Color(self._profile_color.r, self._profile_color.g, self._profile_color.b, int(230 * alpha)))
|
||||
|
||||
value = f"+{boost:.2f}"
|
||||
value_size = measure_text_cached(self._font, value, VALUE_FONT_SIZE)
|
||||
rl.draw_text_ex(self._font, value, rl.Vector2(cx - value_size.x / 2, track_top - 30), VALUE_FONT_SIZE, 0,
|
||||
rl.Color(COLOR_TEXT.r, COLOR_TEXT.g, COLOR_TEXT.b, int(COLOR_TEXT.a * alpha)))
|
||||
@@ -5,13 +5,13 @@ from opendbc.car.structs import car
|
||||
from openpilot.cereal.visionipc import VisionStreamType
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH
|
||||
from openpilot.selfdrive.ui.mici.onroad.accel_boost_indicator import AccelBoostIndicator
|
||||
from openpilot.selfdrive.ui.mici.onroad.alert_renderer import AlertRenderer
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
|
||||
from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer
|
||||
from openpilot.selfdrive.ui.mici.onroad.long_indicator import LongIndicator
|
||||
from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer
|
||||
from openpilot.selfdrive.ui.mici.onroad.navigation import NavigationPanel, PANEL_WIDTH
|
||||
from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall
|
||||
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent, TextAlignment, TextAlignmentVertical
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
@@ -180,7 +180,7 @@ class AugmentedRoadView(CameraView):
|
||||
self._long_indicator = LongIndicator()
|
||||
self._alert_renderer = AlertRenderer()
|
||||
self._driver_state_renderer = DriverStateRenderer()
|
||||
self._confidence_ball = ConfidenceBall()
|
||||
self._accel_boost_indicator = AccelBoostIndicator()
|
||||
self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY,
|
||||
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||
alignment=TextAlignment.CENTER,
|
||||
@@ -277,7 +277,7 @@ class AugmentedRoadView(CameraView):
|
||||
if show_navigation:
|
||||
self._navigation.render(rl.Rectangle(self._content_rect.x + self._content_rect.width, self.rect.y, PANEL_WIDTH, self.rect.height))
|
||||
else:
|
||||
self._confidence_ball.render(self.rect)
|
||||
self._accel_boost_indicator.render(self.rect)
|
||||
|
||||
self._bookmark_icon.render(self.rect)
|
||||
|
||||
|
||||
@@ -30,6 +30,11 @@ ACC_NOLONG_DESCRIPTION = tr_noop("This feature can only be used with sunnypilot
|
||||
ACC_PCMCRUISE_DISABLED_DESCRIPTION = tr_noop("This feature is not supported on this platform due to vehicle limitations.")
|
||||
ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.")
|
||||
|
||||
ACCEL_BOOST_EXT_DESCRIPTION = tr_noop("Adds extra acceleration on top of the stock gas-override boost when you hold the gas pedal " +
|
||||
"while sunnypilot longitudinal control is active. The extra boost follows the Following Distance setting: " +
|
||||
"strongest when Aggressive, weakest when Relaxed. The stock boost remains active when this is off.")
|
||||
ACCEL_BOOST_EXT_NOLONG_DESCRIPTION = tr_noop("This feature requires sunnypilot longitudinal control to be available.")
|
||||
|
||||
|
||||
class CruiseLayout(Widget):
|
||||
def __init__(self):
|
||||
@@ -76,6 +81,11 @@ class CruiseLayout(Widget):
|
||||
min_value=1, max_value=3, value_change_step=1,
|
||||
inline=True)
|
||||
|
||||
self.accel_boost_toggle = toggle_item_sp(
|
||||
title=tr("Extra Acceleration Boost by Following Distance"),
|
||||
description="",
|
||||
param="FollowDistanceAccelBoost")
|
||||
|
||||
self.sla_settings_button = simple_button_item_sp(
|
||||
button_text=lambda: tr("Speed Limit"),
|
||||
button_width=800,
|
||||
@@ -89,6 +99,7 @@ class CruiseLayout(Widget):
|
||||
self.custom_acc_toggle,
|
||||
self.custom_acc_short_increment,
|
||||
self.custom_acc_long_increment,
|
||||
self.accel_boost_toggle,
|
||||
self.sla_settings_button,
|
||||
]
|
||||
return items
|
||||
@@ -104,6 +115,7 @@ class CruiseLayout(Widget):
|
||||
self._scroller.show_event()
|
||||
self.icbm_toggle.show_description(True)
|
||||
self.custom_acc_toggle.show_description(True)
|
||||
self.accel_boost_toggle.show_description(True)
|
||||
|
||||
def _set_current_panel(self, panel: PanelType):
|
||||
self._current_panel = panel
|
||||
@@ -148,10 +160,17 @@ class CruiseLayout(Widget):
|
||||
self.scc_v_toggle.action_item.set_enabled(False)
|
||||
self.scc_m_toggle.action_item.set_enabled(False)
|
||||
|
||||
self.accel_boost_toggle.action_item.set_enabled(has_long and ui_state.is_offroad())
|
||||
boost_desc = tr(ACCEL_BOOST_EXT_DESCRIPTION) if has_long else tr(ACCEL_BOOST_EXT_NOLONG_DESCRIPTION)
|
||||
if self.accel_boost_toggle.description != boost_desc:
|
||||
self.accel_boost_toggle.set_description(boost_desc)
|
||||
self.accel_boost_toggle.show_description(True)
|
||||
|
||||
else:
|
||||
has_icbm = has_long = False
|
||||
self.icbm_toggle.action_item.set_enabled(False)
|
||||
self.icbm_toggle.set_description(tr(ONROAD_ONLY_DESCRIPTION))
|
||||
self.accel_boost_toggle.action_item.set_enabled(False)
|
||||
|
||||
show_custom_acc_desc = False
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from opendbc.car.toyota.values import ToyotaFlags
|
||||
from opendbc.sunnypilot.car.toyota.values import ToyotaFlagsSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
@@ -15,6 +17,10 @@ from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
|
||||
ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.")
|
||||
SNG_HACK_UNAVAILABLE = tr_noop("sunnypilot Longitudinal Control must be available and enabled for your vehicle to use this feature.")
|
||||
BSM_UNAVAILABLE = tr_noop("Enhanced Blind Spot Monitoring requires a TSS2 Toyota/Lexus without SecOC.")
|
||||
BSM_STOCK_LONG_UNAVAILABLE = tr_noop("Enhanced Blind Spot Monitoring is not available while Enforce Factory Longitudinal Control is enabled.")
|
||||
BSM_REQUIRED = tr_noop("This vehicle does not report blind spot status over the standard CAN signals. " +
|
||||
"Enhanced Blind Spot Monitoring is required for blind spot detection.")
|
||||
|
||||
DESCRIPTIONS = {
|
||||
'enforce_stock_longitudinal': tr_noop(
|
||||
@@ -23,6 +29,10 @@ DESCRIPTIONS = {
|
||||
'stop_and_go_hack': tr_noop(
|
||||
'sunnypilot will allow some Toyota/Lexus cars to auto resume during stop and go traffic. ' +
|
||||
'This feature is only applicable to certain models that are able to use longitudinal control. This is an alpha feature. Use at your own risk.'
|
||||
),
|
||||
'enhanced_bsm': tr_noop(
|
||||
'sunnypilot will poll the blind spot monitor diagnostics directly from the radar. Use this on vehicles whose blind spot monitors ' +
|
||||
'do not report blind spot status over the standard CAN signals. This is an alpha feature. Use at your own risk.'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,9 +57,18 @@ class ToyotaSettings(BrandSettings):
|
||||
enabled=lambda: not ui_state.engaged,
|
||||
)
|
||||
|
||||
self.enhanced_bsm = toggle_item_sp(
|
||||
lambda: tr("Enhanced Blind Spot Monitoring (Alpha)"),
|
||||
description=lambda: tr(DESCRIPTIONS["enhanced_bsm"]),
|
||||
initial_state=ui_state.params.get_bool("ToyotaEnhancedBsm"),
|
||||
callback=self._on_enable_enhanced_bsm,
|
||||
enabled=lambda: not ui_state.engaged,
|
||||
)
|
||||
|
||||
self.items = [
|
||||
self.enforce_stock_longitudinal,
|
||||
self.stop_and_go_hack,
|
||||
self.enhanced_bsm,
|
||||
]
|
||||
|
||||
def _on_enable_enforce_stock_longitudinal(self, state: bool):
|
||||
@@ -94,6 +113,25 @@ class ToyotaSettings(BrandSettings):
|
||||
ui_state.params.put_bool("ToyotaStopAndGoHack", False)
|
||||
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def _on_enable_enhanced_bsm(self, state: bool):
|
||||
if state:
|
||||
def confirm_callback(result: int):
|
||||
if result == DialogResult.CONFIRM:
|
||||
ui_state.params.put_bool("ToyotaEnhancedBsm", True)
|
||||
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||
else:
|
||||
self.enhanced_bsm.action_item.set_state(False)
|
||||
|
||||
content = (f"<h1>{self.enhanced_bsm.title}</h1><br>" +
|
||||
f"<p>{self.enhanced_bsm.description}</p>")
|
||||
|
||||
dlg = ConfirmDialog(content, tr("Enable"), rich=True, callback=confirm_callback)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
else:
|
||||
ui_state.params.put_bool("ToyotaEnhancedBsm", False)
|
||||
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def update_settings(self):
|
||||
if ui_state.CP is not None:
|
||||
longitudinal = ui_state.CP.openpilotLongitudinalControl
|
||||
@@ -113,9 +151,36 @@ class ToyotaSettings(BrandSettings):
|
||||
self.stop_and_go_hack.set_description(new_desc)
|
||||
if show_desc:
|
||||
self.stop_and_go_hack.show_description(True)
|
||||
|
||||
# Enhanced BSM needs the radar diagnostic address, which is only in the TX allowlist for
|
||||
# TSS2 cars without SecOC that are not enforcing factory longitudinal control
|
||||
bsm_supported = bool(ui_state.CP.flags & ToyotaFlags.TSS2) and not bool(ui_state.CP.flags & ToyotaFlags.SECOC)
|
||||
bsm_available = bsm_supported and not enforce_stock
|
||||
self.enhanced_bsm.action_item.set_enabled(bsm_available and not ui_state.engaged)
|
||||
|
||||
bsm_desc = tr(DESCRIPTIONS["enhanced_bsm"])
|
||||
if not bsm_supported:
|
||||
self.enhanced_bsm.action_item.set_state(False)
|
||||
bsm_desc = "<b>" + tr(BSM_UNAVAILABLE) + "</b>\n\n" + bsm_desc
|
||||
elif enforce_stock:
|
||||
self.enhanced_bsm.action_item.set_state(False)
|
||||
bsm_desc = "<b>" + tr(BSM_STOCK_LONG_UNAVAILABLE) + "</b>\n\n" + bsm_desc
|
||||
elif ui_state.CP_SP is not None and ui_state.CP_SP.flags & ToyotaFlagsSP.SP_NEED_DEBUG_BSM:
|
||||
bsm_desc = "<b>" + tr(BSM_REQUIRED) + "</b>\n\n" + bsm_desc
|
||||
|
||||
if self.enhanced_bsm.description != bsm_desc:
|
||||
self.enhanced_bsm.set_description(bsm_desc)
|
||||
if not bsm_available:
|
||||
self.enhanced_bsm.show_description(True)
|
||||
else:
|
||||
self.stop_and_go_hack.action_item.set_enabled(False)
|
||||
new_desc = "<b>" + tr(ONROAD_ONLY_DESCRIPTION) + "</b>\n\n" + tr(DESCRIPTIONS["stop_and_go_hack"])
|
||||
if self.stop_and_go_hack.description != new_desc:
|
||||
self.stop_and_go_hack.set_description(new_desc)
|
||||
self.stop_and_go_hack.show_description(True)
|
||||
|
||||
self.enhanced_bsm.action_item.set_enabled(False)
|
||||
bsm_desc = "<b>" + tr(ONROAD_ONLY_DESCRIPTION) + "</b>\n\n" + tr(DESCRIPTIONS["enhanced_bsm"])
|
||||
if self.enhanced_bsm.description != bsm_desc:
|
||||
self.enhanced_bsm.set_description(bsm_desc)
|
||||
self.enhanced_bsm.show_description(True)
|
||||
|
||||
@@ -31,7 +31,6 @@ class Laneful: # TODO-SP: Rename this to blendful or something similar because i
|
||||
self.usual_offset = FirstOrderFilter(0.0, 10.0, DT_MDL, initialized=False)
|
||||
self.going_around = False
|
||||
self.trust = 0.0
|
||||
self.model_curvature = 0.0
|
||||
self.curvature = 0.0
|
||||
|
||||
def update(self, model_output: dict[str, np.ndarray], v_ego: float, active: bool, model_curvature: float) -> float:
|
||||
@@ -41,7 +40,6 @@ class Laneful: # TODO-SP: Rename this to blendful or something similar because i
|
||||
self.usual_offset.initialized = False
|
||||
self.lane_width.initialized = False
|
||||
self.going_around = False
|
||||
self.model_curvature = model_curvature
|
||||
target = 0.0 if fading else self._target(model_output, v_ego, model_curvature)
|
||||
releasing = not fading and (abs(target) < abs(self.curvature) or target * self.curvature < 0)
|
||||
step = (2.0 if releasing else 0.5) / max(v_ego, 1.0) ** 2 * DT_MDL # jerk over 1 second
|
||||
|
||||
@@ -510,9 +510,6 @@ def main(demo=False):
|
||||
mdv2sp_send.valid = modelv2_send.valid
|
||||
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
||||
mdv2sp_send.modelDataV2SP.lanefulCurvature = laneful.curvature
|
||||
mdv2sp_send.modelDataV2SP.lanefulTrust = laneful.trust
|
||||
mdv2sp_send.modelDataV2SP.modelCurvature = laneful.model_curvature
|
||||
mdv2sp_send.modelDataV2SP.lanefulGoingAround = laneful.going_around
|
||||
drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state
|
||||
drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ def initialize_params(params) -> list[dict[str, Any]]:
|
||||
keys.extend([
|
||||
"ToyotaEnforceStockLongitudinal",
|
||||
"ToyotaStopAndGoHack",
|
||||
"ToyotaEnhancedBsm",
|
||||
])
|
||||
|
||||
return [{k: params.get(k, return_default=True)} for k in keys]
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.cereal import log
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.controls.lib.accel_boost import AccelBoost, ACCEL_BOOST_MAX
|
||||
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
|
||||
|
||||
SCALE_RELAXED = 0.3
|
||||
SCALE_STANDARD = 0.5
|
||||
SCALE_AGGRESSIVE = 0.7
|
||||
|
||||
|
||||
def get_accel_boost_scale(personality) -> float:
|
||||
if personality == log.LongitudinalPersonality.aggressive:
|
||||
return SCALE_AGGRESSIVE
|
||||
if personality == log.LongitudinalPersonality.relaxed:
|
||||
return SCALE_RELAXED
|
||||
return SCALE_STANDARD
|
||||
|
||||
|
||||
def get_accel_boost_max(personality) -> float:
|
||||
"""Maximum reported boost: the stock cap plus the profile-scaled extra cap."""
|
||||
return ACCEL_BOOST_MAX * (1.0 + get_accel_boost_scale(personality))
|
||||
|
||||
|
||||
class AccelBoostExt:
|
||||
def __init__(self, stock: AccelBoost | None = None):
|
||||
self._stock = stock if stock is not None else AccelBoost()
|
||||
self._extra = AccelBoost()
|
||||
self.params = Params()
|
||||
self.frame = -1
|
||||
self.enabled = self.params.get_bool("FollowDistanceAccelBoost")
|
||||
|
||||
self.stock_boost = 0.0
|
||||
self.extra_boost = 0.0
|
||||
|
||||
@property
|
||||
def total_boost(self) -> float:
|
||||
return self._stock.total_boost + self.extra_boost
|
||||
|
||||
def _update_params(self) -> None:
|
||||
if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0:
|
||||
self.enabled = self.params.get_bool("FollowDistanceAccelBoost")
|
||||
|
||||
def update(self, sm, output_a_target_e2e, output_a_target_mpc, a_cruise):
|
||||
self.frame += 1
|
||||
self._update_params()
|
||||
|
||||
if self.enabled and sm['selfdriveState'].enabled:
|
||||
scale = get_accel_boost_scale(sm['selfdriveState'].personality)
|
||||
extra_output = self._extra.update(sm, output_a_target_e2e, output_a_target_mpc, a_cruise)
|
||||
self.extra_boost = (extra_output - output_a_target_e2e) * scale
|
||||
else:
|
||||
self.extra_boost = 0.0
|
||||
self._extra.total_boost = 0.0
|
||||
self._extra.boost_this_override = 0.0
|
||||
self._extra.boost_eligible = False
|
||||
|
||||
target = output_a_target_e2e + self.extra_boost
|
||||
output = self._stock.update(sm, target, output_a_target_mpc, a_cruise)
|
||||
self.stock_boost = output - target
|
||||
return output
|
||||
@@ -9,6 +9,7 @@ from openpilot.cereal import messaging, custom
|
||||
from opendbc.car import structs
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_boost_ext import AccelBoostExt
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.e2e_alerts_helper import E2EAlertsHelper
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.smart_cruise_control import SmartCruiseControl
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist
|
||||
@@ -32,6 +33,16 @@ class LongitudinalPlannerSP:
|
||||
|
||||
self.output_v_target = 0.
|
||||
self.output_a_target = 0.
|
||||
self._accel_boost: AccelBoostExt | None = None
|
||||
|
||||
@property
|
||||
def accel_boost(self) -> AccelBoostExt:
|
||||
assert self._accel_boost is not None
|
||||
return self._accel_boost
|
||||
|
||||
@accel_boost.setter
|
||||
def accel_boost(self, controller) -> None:
|
||||
self._accel_boost = AccelBoostExt(controller)
|
||||
|
||||
def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]:
|
||||
CS = sm['carState']
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
from openpilot.cereal import log
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.selfdrive.controls.lib.accel_boost import AccelBoost, ACCEL_BOOST_MAX, ACCEL_BOOST_PER_OVERRIDE
|
||||
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_boost_ext import AccelBoostExt, get_accel_boost_max
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlannerSP
|
||||
|
||||
E2E_ACCEL = 0.5
|
||||
PLANNER_ACCEL = 2.0
|
||||
STEPS_PER_OVERRIDE = round(ACCEL_BOOST_PER_OVERRIDE / (0.025 * DT_MDL))
|
||||
EXPECTED_SCALE = {
|
||||
log.LongitudinalPersonality.relaxed: 0.3,
|
||||
log.LongitudinalPersonality.standard: 0.5,
|
||||
log.LongitudinalPersonality.aggressive: 0.7,
|
||||
}
|
||||
|
||||
|
||||
class FakeParams:
|
||||
def __init__(self, enabled=True):
|
||||
self.enabled = enabled
|
||||
|
||||
def get_bool(self, key):
|
||||
return self.enabled
|
||||
|
||||
|
||||
class FakeSubMaster:
|
||||
def __init__(self, enabled=True, gas_pressed=False, v_ego=15.0,
|
||||
personality=log.LongitudinalPersonality.standard):
|
||||
self._messages = {
|
||||
'selfdriveState': SimpleNamespace(enabled=enabled, personality=personality),
|
||||
'carState': SimpleNamespace(gasPressed=gas_pressed, vEgo=v_ego),
|
||||
}
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._messages[key]
|
||||
|
||||
|
||||
def update(boost, sm, e2e=E2E_ACCEL):
|
||||
return boost.update(sm, e2e, PLANNER_ACCEL, PLANNER_ACCEL)
|
||||
|
||||
|
||||
class PlannerStub(LongitudinalPlannerSP):
|
||||
def __init__(self):
|
||||
self._accel_boost = None
|
||||
|
||||
|
||||
class TestAccelBoostExt(OpenpilotTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.params = FakeParams()
|
||||
patcher = mock.patch("openpilot.sunnypilot.selfdrive.controls.lib.accel_boost_ext.Params", return_value=self.params)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def _boost_to_max(self, personality):
|
||||
boost = AccelBoostExt()
|
||||
sm = FakeSubMaster(personality=personality)
|
||||
# Eligibility is latched on a frame without gas, then applied on the next override.
|
||||
update(boost, sm)
|
||||
for _ in range(10):
|
||||
sm['carState'].gasPressed = True
|
||||
for _ in range(STEPS_PER_OVERRIDE):
|
||||
update(boost, sm)
|
||||
sm['carState'].gasPressed = False
|
||||
update(boost, sm)
|
||||
return boost, sm
|
||||
|
||||
def _applied_extra(self, boost, sm):
|
||||
update(boost, sm)
|
||||
return boost.extra_boost
|
||||
|
||||
def test_off_matches_stock_boost_exactly(self):
|
||||
self.params.enabled = False
|
||||
sm = FakeSubMaster()
|
||||
stock = AccelBoost()
|
||||
ext = AccelBoostExt()
|
||||
|
||||
stock_out = update(stock, sm)
|
||||
ext_out = update(ext, sm)
|
||||
self.assertAlmostEqual(ext_out, stock_out)
|
||||
|
||||
for _ in range(10):
|
||||
sm['carState'].gasPressed = True
|
||||
for _ in range(STEPS_PER_OVERRIDE):
|
||||
stock_out = update(stock, sm)
|
||||
ext_out = update(ext, sm)
|
||||
self.assertAlmostEqual(ext_out, stock_out)
|
||||
sm['carState'].gasPressed = False
|
||||
stock_out = update(stock, sm)
|
||||
ext_out = update(ext, sm)
|
||||
self.assertAlmostEqual(ext_out, stock_out)
|
||||
|
||||
self.assertAlmostEqual(ext.total_boost, stock.total_boost)
|
||||
self.assertAlmostEqual(ext.stock_boost, stock_out - E2E_ACCEL)
|
||||
self.assertEqual(ext.extra_boost, 0.0)
|
||||
self.assertEqual(ext._extra.total_boost, 0.0)
|
||||
|
||||
def test_no_boost_without_gas(self):
|
||||
ext = AccelBoostExt()
|
||||
sm = FakeSubMaster(gas_pressed=False)
|
||||
out = update(ext, sm)
|
||||
self.assertEqual(ext.total_boost, 0.0)
|
||||
self.assertEqual(ext._extra.total_boost, 0.0)
|
||||
self.assertAlmostEqual(out, E2E_ACCEL)
|
||||
|
||||
def test_boost_requires_e2e_to_be_limiting(self):
|
||||
ext = AccelBoostExt()
|
||||
sm = FakeSubMaster(gas_pressed=False)
|
||||
ext.update(sm, E2E_ACCEL, E2E_ACCEL, E2E_ACCEL)
|
||||
sm['carState'].gasPressed = True
|
||||
for _ in range(10):
|
||||
ext.update(sm, E2E_ACCEL, E2E_ACCEL, E2E_ACCEL)
|
||||
self.assertEqual(ext.total_boost, 0.0)
|
||||
self.assertEqual(ext.stock_boost, 0.0)
|
||||
self.assertEqual(ext.extra_boost, 0.0)
|
||||
self.assertEqual(ext._extra.total_boost, 0.0)
|
||||
|
||||
def test_extra_layer_per_override_cap(self):
|
||||
ext = AccelBoostExt()
|
||||
sm = FakeSubMaster(gas_pressed=False)
|
||||
update(ext, sm)
|
||||
sm['carState'].gasPressed = True
|
||||
for _ in range(1000):
|
||||
update(ext, sm)
|
||||
|
||||
self.assertAlmostEqual(ext._extra.total_boost, ACCEL_BOOST_PER_OVERRIDE)
|
||||
applied = self._applied_extra(ext, sm)
|
||||
expected = ACCEL_BOOST_PER_OVERRIDE * EXPECTED_SCALE[log.LongitudinalPersonality.standard]
|
||||
self.assertAlmostEqual(applied, expected)
|
||||
|
||||
def test_extra_scales_with_following_distance(self):
|
||||
applied = {}
|
||||
for personality in (log.LongitudinalPersonality.relaxed,
|
||||
log.LongitudinalPersonality.standard,
|
||||
log.LongitudinalPersonality.aggressive):
|
||||
ext, sm = self._boost_to_max(personality)
|
||||
sm['carState'].gasPressed = True
|
||||
applied[personality] = self._applied_extra(ext, sm)
|
||||
|
||||
for personality, value in applied.items():
|
||||
self.assertAlmostEqual(value, ACCEL_BOOST_MAX * EXPECTED_SCALE[personality])
|
||||
|
||||
self.assertLess(applied[log.LongitudinalPersonality.relaxed], applied[log.LongitudinalPersonality.standard])
|
||||
self.assertLess(applied[log.LongitudinalPersonality.standard], applied[log.LongitudinalPersonality.aggressive])
|
||||
|
||||
def test_capnp_dynamic_enum_personality_scales(self):
|
||||
# Real SubMaster messages expose personality as a capnp DynamicEnum; equality still matches the enum value.
|
||||
msg = log.SelfdriveState.new_message()
|
||||
msg.personality = log.LongitudinalPersonality.aggressive
|
||||
ext, sm = self._boost_to_max(msg.personality)
|
||||
sm['carState'].gasPressed = True
|
||||
applied = self._applied_extra(ext, sm)
|
||||
self.assertAlmostEqual(applied, ACCEL_BOOST_MAX * EXPECTED_SCALE[log.LongitudinalPersonality.aggressive])
|
||||
|
||||
def test_extra_feeds_into_stock_last(self):
|
||||
ext, sm = self._boost_to_max(log.LongitudinalPersonality.standard)
|
||||
sm['carState'].gasPressed = True
|
||||
inputs = []
|
||||
original_update = AccelBoost.update
|
||||
|
||||
def spy(boost, sm_, e2e, mpc, cruise):
|
||||
inputs.append(e2e)
|
||||
return original_update(boost, sm_, e2e, mpc, cruise)
|
||||
|
||||
with mock.patch.object(AccelBoost, "update", spy):
|
||||
out = update(ext, sm)
|
||||
|
||||
# First call is our extra layer on the raw target, last call is stock on the boosted target.
|
||||
self.assertAlmostEqual(inputs[0], E2E_ACCEL)
|
||||
self.assertAlmostEqual(inputs[-1], E2E_ACCEL + ext.extra_boost)
|
||||
self.assertAlmostEqual(out, inputs[-1] + ext.stock_boost)
|
||||
|
||||
def test_extra_is_added_on_top_of_stock_boost(self):
|
||||
ext, sm = self._boost_to_max(log.LongitudinalPersonality.aggressive)
|
||||
sm['carState'].gasPressed = True
|
||||
out = update(ext, sm)
|
||||
extra = out - E2E_ACCEL - ext.stock_boost
|
||||
self.assertAlmostEqual(ext._stock.total_boost, ACCEL_BOOST_MAX)
|
||||
self.assertAlmostEqual(ext.total_boost, ACCEL_BOOST_MAX + ext.extra_boost)
|
||||
self.assertAlmostEqual(ext.stock_boost, ACCEL_BOOST_MAX)
|
||||
self.assertAlmostEqual(ext.extra_boost, ACCEL_BOOST_MAX * EXPECTED_SCALE[log.LongitudinalPersonality.aggressive])
|
||||
self.assertAlmostEqual(extra, ext.extra_boost)
|
||||
|
||||
def test_sp_planner_wraps_assigned_stock_controller(self):
|
||||
planner = PlannerStub()
|
||||
stock = AccelBoost()
|
||||
planner.accel_boost = stock
|
||||
|
||||
self.assertIsInstance(planner.accel_boost, AccelBoostExt)
|
||||
self.assertIs(planner.accel_boost._stock, stock)
|
||||
|
||||
def test_wraps_existing_stock_controller(self):
|
||||
stock = AccelBoost()
|
||||
sm = FakeSubMaster()
|
||||
update(stock, sm)
|
||||
sm['carState'].gasPressed = True
|
||||
update(stock, sm)
|
||||
|
||||
ext = AccelBoostExt(stock)
|
||||
self.assertIs(ext._stock, stock)
|
||||
self.assertAlmostEqual(ext.total_boost, stock.total_boost)
|
||||
|
||||
def test_boost_bar_max_matches_profile_cap(self):
|
||||
for personality, scale in EXPECTED_SCALE.items():
|
||||
self.assertAlmostEqual(get_accel_boost_max(personality), ACCEL_BOOST_MAX * (1.0 + scale))
|
||||
|
||||
def test_total_boost_is_stock_accumulated_plus_extra_applied(self):
|
||||
ext, sm = self._boost_to_max(log.LongitudinalPersonality.aggressive)
|
||||
sm['carState'].gasPressed = True
|
||||
update(ext, sm)
|
||||
self.assertAlmostEqual(ext.total_boost, ext._stock.total_boost + ext.extra_boost)
|
||||
|
||||
def test_turning_toggle_off_restores_stock_only(self):
|
||||
ext, sm = self._boost_to_max(log.LongitudinalPersonality.aggressive)
|
||||
self.assertGreater(ext._extra.total_boost, 0.0)
|
||||
|
||||
self.params.enabled = False
|
||||
sm['carState'].gasPressed = True
|
||||
# Params are re-read every PARAMS_UPDATE_PERIOD; the extra layer must clear on the next poll.
|
||||
for _ in range(int(PARAMS_UPDATE_PERIOD / DT_MDL) + 1):
|
||||
out = update(ext, sm)
|
||||
|
||||
self.assertEqual(ext._extra.total_boost, 0.0)
|
||||
self.assertEqual(ext.extra_boost, 0.0)
|
||||
self.assertAlmostEqual(out, E2E_ACCEL + ext.stock_boost)
|
||||
|
||||
def test_relaxing_following_distance_applies_immediately(self):
|
||||
ext, sm = self._boost_to_max(log.LongitudinalPersonality.aggressive)
|
||||
sm['selfdriveState'].personality = log.LongitudinalPersonality.relaxed
|
||||
sm['carState'].gasPressed = True
|
||||
applied = self._applied_extra(ext, sm)
|
||||
self.assertAlmostEqual(applied, ACCEL_BOOST_MAX * EXPECTED_SCALE[log.LongitudinalPersonality.relaxed])
|
||||
@@ -10,6 +10,7 @@ from openpilot.cereal import custom, messaging
|
||||
from opendbc.car.structs import car
|
||||
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, UNSUPPORTED_LONGITUDINAL_CAR
|
||||
from opendbc.car.subaru.values import CAR as SUBARU_CAR, SubaruFlags
|
||||
from opendbc.car.toyota.values import CAR as TOYOTA_CAR, ToyotaFlags
|
||||
from opendbc.sunnypilot.car.interfaces import has_bsm
|
||||
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
|
||||
from openpilot.common.params import Params
|
||||
@@ -43,6 +44,7 @@ CAPABILITY_FIELDS = (
|
||||
"device_type",
|
||||
"subaru_has_sng",
|
||||
"hyundai_alpha_long_available",
|
||||
"toyota_enhanced_bsm_available",
|
||||
)
|
||||
|
||||
CAPABILITY_LABELS: dict[str, str] = {
|
||||
@@ -65,6 +67,7 @@ CAPABILITY_LABELS: dict[str, str] = {
|
||||
"device_type": "Device type",
|
||||
"subaru_has_sng": "Subaru Stop-and-Go available",
|
||||
"hyundai_alpha_long_available": "Hyundai Alpha Longitudinal available",
|
||||
"toyota_enhanced_bsm_available": "Toyota Enhanced BSM available",
|
||||
}
|
||||
|
||||
# Explicit defaults for non-boolean capability fields
|
||||
@@ -111,6 +114,16 @@ def _resolve_brand_capabilities(caps: dict, bundle_platform: str, CP) -> None:
|
||||
caps["subaru_has_sng"] = not bool(CP.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
|
||||
caps["has_stop_and_go"] = caps["subaru_has_sng"]
|
||||
|
||||
elif brand == "toyota":
|
||||
if bundle_platform:
|
||||
try:
|
||||
flags = TOYOTA_CAR[bundle_platform].config.flags
|
||||
caps["toyota_enhanced_bsm_available"] = bool(flags & ToyotaFlags.TSS2) and not bool(flags & ToyotaFlags.SECOC)
|
||||
except KeyError:
|
||||
cloudlog.exception(f"capabilities: unknown toyota platform {bundle_platform!r}")
|
||||
elif CP is not None:
|
||||
caps["toyota_enhanced_bsm_available"] = bool(CP.flags & ToyotaFlags.TSS2) and not bool(CP.flags & ToyotaFlags.SECOC)
|
||||
|
||||
|
||||
def generate_capabilities(params: Params | None = None) -> dict:
|
||||
"""Generate a SettingsCapabilities dict from CarParams + boolean params.
|
||||
|
||||
@@ -156,7 +156,7 @@ The tables below describe the **compiled** `settings_ui.json` schema — what th
|
||||
|
||||
**Enablement rules**: Grayed out (disabled) when rules fail. Frontend shows a contextual badge explaining why.
|
||||
|
||||
**Capability fields** (referenced in rules): `has_longitudinal_control`, `has_icbm`, `icbm_available`, `torque_allowed`, `brand`, `pcm_cruise`, `alpha_long_available`, `steer_control_type`, `has_bsm`, `is_release`, `is_sp_release`, `is_development`, `tesla_has_vehicle_bus`, `has_stop_and_go`, `stock_longitudinal`
|
||||
**Capability fields** (referenced in rules): `has_longitudinal_control`, `has_icbm`, `icbm_available`, `torque_allowed`, `brand`, `pcm_cruise`, `alpha_long_available`, `steer_control_type`, `has_bsm`, `is_release`, `is_sp_release`, `is_development`, `tesla_has_vehicle_bus`, `has_stop_and_go`, `stock_longitudinal`, `toyota_enhanced_bsm_available`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -620,6 +620,22 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "FollowDistanceAccelBoost",
|
||||
"widget": "toggle",
|
||||
"title": "Extra Acceleration Boost by Following Distance",
|
||||
"description": "Adds extra acceleration on top of the stock gas-override boost. The extra boost follows the Following Distance setting, strongest when Aggressive and weakest when Relaxed. The stock boost remains active when this is off.",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "offroad_only"
|
||||
},
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "has_longitudinal_control",
|
||||
"equals": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "IntelligentCruiseButtonManagement",
|
||||
"widget": "toggle",
|
||||
@@ -2324,6 +2340,28 @@
|
||||
"equals": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ToyotaEnhancedBsm",
|
||||
"widget": "toggle",
|
||||
"needs_onroad_cycle": true,
|
||||
"title": "Enhanced Blind Spot Monitoring (Alpha)",
|
||||
"description": "sunnypilot will poll the blind spot monitor diagnostics directly from the radar. Use this on vehicles whose blind spot monitors do not report blind spot status over the standard CAN signals. This is an alpha feature. Use at your own risk.",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "not_engaged"
|
||||
},
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "toyota_enhanced_bsm_available",
|
||||
"equals": true
|
||||
},
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "stock_longitudinal",
|
||||
"equals": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -30,6 +30,15 @@ sections:
|
||||
label: Relaxed
|
||||
enablement:
|
||||
- $ref: '#/macros/longitudinal'
|
||||
- key: FollowDistanceAccelBoost
|
||||
widget: toggle
|
||||
title: Extra Acceleration Boost by Following Distance
|
||||
description: Adds extra acceleration on top of the stock gas-override boost. The extra boost follows
|
||||
the Following Distance setting, strongest when Aggressive and weakest when Relaxed. The stock boost
|
||||
remains active when this is off.
|
||||
enablement:
|
||||
- $ref: '#/macros/offroad'
|
||||
- $ref: '#/macros/longitudinal'
|
||||
- key: IntelligentCruiseButtonManagement
|
||||
widget: toggle
|
||||
title: Intelligent Cruise Button Management (ICBM) (Alpha)
|
||||
|
||||
@@ -101,3 +101,18 @@ sections:
|
||||
- type: param
|
||||
key: ToyotaEnforceStockLongitudinal
|
||||
equals: false
|
||||
- key: ToyotaEnhancedBsm
|
||||
widget: toggle
|
||||
needs_onroad_cycle: true
|
||||
title: Enhanced Blind Spot Monitoring (Alpha)
|
||||
description: sunnypilot will poll the blind spot monitor diagnostics directly from the radar. Use this on vehicles whose
|
||||
blind spot monitors do not report blind spot status over the standard CAN signals. This is an alpha feature. Use at
|
||||
your own risk.
|
||||
enablement:
|
||||
- $ref: '#/macros/not_engaged'
|
||||
- type: capability
|
||||
field: toyota_enhanced_bsm_available
|
||||
equals: true
|
||||
- type: capability
|
||||
field: stock_longitudinal
|
||||
equals: false
|
||||
|
||||
@@ -69,12 +69,18 @@ class TestOpaquePerBrandFlags(OpenpilotTestCase):
|
||||
def test_hyundai_alpha_long_available_field_present(self):
|
||||
assert "hyundai_alpha_long_available" in CAPABILITY_FIELDS
|
||||
|
||||
def test_toyota_enhanced_bsm_available_field_present(self):
|
||||
assert "toyota_enhanced_bsm_available" in CAPABILITY_FIELDS
|
||||
|
||||
def test_subaru_has_sng_default_false(self, caps):
|
||||
assert caps["subaru_has_sng"] is False
|
||||
|
||||
def test_hyundai_alpha_long_available_default_false(self, caps):
|
||||
assert caps["hyundai_alpha_long_available"] is False
|
||||
|
||||
def test_toyota_enhanced_bsm_available_default_false(self, caps):
|
||||
assert caps["toyota_enhanced_bsm_available"] is False
|
||||
|
||||
|
||||
class TestCapabilitiesShape(OpenpilotTestCase):
|
||||
def test_all_fields_present(self, caps):
|
||||
|
||||
@@ -207,6 +207,7 @@ class TestNotEngagedReplacement(OpenpilotTestCase):
|
||||
"AlphaLongitudinalEnabled",
|
||||
"ToyotaEnforceStockLongitudinal",
|
||||
"ToyotaStopAndGoHack",
|
||||
"ToyotaEnhancedBsm",
|
||||
], names=["key"])
|
||||
def test_offroad_only_replaced_with_not_engaged(self, schema, key):
|
||||
"""These items should use not_engaged, not offroad_only."""
|
||||
|
||||
@@ -286,6 +286,7 @@ class TestKnownVehicleSettings(OpenpilotTestCase):
|
||||
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("toyota"))}
|
||||
assert "ToyotaEnforceStockLongitudinal" in keys
|
||||
assert "ToyotaStopAndGoHack" in keys
|
||||
assert "ToyotaEnhancedBsm" in keys
|
||||
|
||||
def test_tesla_has_coop_steering(self, schema):
|
||||
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("tesla"))}
|
||||
|
||||
Reference in New Issue
Block a user