Compare commits

..

16 Commits

Author SHA1 Message Date
rav4kumar be3bd6bafd long: gate boost param and drop dead state 2026-10-26 12:20:08 -07:00
rav4kumar 663e9c5e42 [MICI] ui: move and fix boost bar 2026-10-26 12:20:00 -07:00
rav4kumar de56d68fa8 Revert "debug"
This reverts commit 07bb42c2bb.
2026-10-25 11:58:12 -07:00
Kumar b914f168b4 Merge branch 'master' into boooost 2026-10-25 11:53:53 -07:00
rav4kumar 07bb42c2bb debug 2026-10-22 12:50:40 -07:00
rav4kumar dd674fbc0d ui improv 2026-10-22 12:49:41 -07:00
rav4kumar 53176484bd early return and no offroad gate 2026-10-22 12:08:09 -07:00
rav4kumar b2d025f543 mici: acccel boost ui 2026-10-22 11:52:46 -07:00
Kumar 35ee46abc3 Merge branch 'master' into boooost 2026-10-22 11:50:21 -07:00
James Vecellio-Grant 567cffdb3e Merge branch 'master' into boooost 2026-10-22 08:31:06 -07:00
Kumar 7cf2f4cf2e Merge branch 'master' into boooost 2026-10-21 11:29:19 -07:00
rav4kumar 1b4902a8ba test: fix boost test collection and stale profile scales 2026-10-20 11:22:54 -07:00
rav4kumar 895bfa8ea5 too much boost lol 2026-10-20 10:59:44 -07:00
rav4kumar 45051dab54 more boost and scale ui per profile 2026-10-20 10:59:44 -07:00
rav4kumar 612c79760a lint 2026-10-20 10:59:44 -07:00
rav4kumar af93efd608 long: add follow distance accel boost 2026-10-20 10:59:44 -07:00
26 changed files with 688 additions and 325 deletions
-30
View File
@@ -50,12 +50,6 @@ jobs:
timeout-minutes: 3 timeout-minutes: 3
run: TARGET_DIR=$STRIPPED_DIR tools/release/build_stripped.sh run: TARGET_DIR=$STRIPPED_DIR tools/release/build_stripped.sh
- run: ./tools/op.sh setup - run: ./tools/op.sh setup
- name: Restore scons cache
uses: actions/cache@v6
with:
path: /tmp/scons_cache
key: scons-${{ runner.os }}-${{ github.job }}-${{ github.sha }}
restore-keys: scons-${{ runner.os }}-${{ github.job }}-
- name: Build openpilot and run checks - name: Build openpilot and run checks
timeout-minutes: 30 timeout-minutes: 30
working-directory: ${{ env.STRIPPED_DIR }} working-directory: ${{ env.STRIPPED_DIR }}
@@ -95,12 +89,6 @@ jobs:
FILTERED=$(echo "$PATH" | tr ':' '\n' | grep -v '/opt/homebrew' | tr '\n' ':') FILTERED=$(echo "$PATH" | tr ':' '\n' | grep -v '/opt/homebrew' | tr '\n' ':')
echo "PATH=${FILTERED}/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" >> $GITHUB_ENV echo "PATH=${FILTERED}/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" >> $GITHUB_ENV
- run: ./tools/op.sh setup - run: ./tools/op.sh setup
- name: Restore scons cache
uses: actions/cache@v6
with:
path: /tmp/scons_cache
key: scons-${{ runner.os }}-${{ github.job }}-${{ github.sha }}
restore-keys: scons-${{ runner.os }}-${{ github.job }}-
- name: Building openpilot - name: Building openpilot
run: scons run: scons
@@ -133,12 +121,6 @@ jobs:
- name: Install raylib headless dependencies - name: Install raylib headless dependencies
if: ${{ !contains(runner.name, 'nsc') }} if: ${{ !contains(runner.name, 'nsc') }}
run: sudo apt-get install -y --no-install-recommends libgles2 libegl1 run: sudo apt-get install -y --no-install-recommends libgles2 libegl1
- name: Restore scons cache
uses: actions/cache@v6
with:
path: /tmp/scons_cache
key: scons-${{ runner.os }}-${{ github.job }}-${{ github.sha }}
restore-keys: scons-${{ runner.os }}-${{ github.job }}-
- name: Build openpilot - name: Build openpilot
run: scons run: scons
- name: Run unit tests - name: Run unit tests
@@ -158,12 +140,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
- run: ./tools/op.sh setup - run: ./tools/op.sh setup
- name: Restore scons cache
uses: actions/cache@v6
with:
path: /tmp/scons_cache
key: scons-${{ runner.os }}-${{ github.job }}-${{ github.sha }}
restore-keys: scons-${{ runner.os }}-${{ github.job }}-
- name: Build openpilot - name: Build openpilot
run: scons run: scons
- name: Run replay - name: Run replay
@@ -252,12 +228,6 @@ jobs:
- name: Install raylib headless dependencies - name: Install raylib headless dependencies
if: ${{ !contains(runner.name, 'nsc') }} if: ${{ !contains(runner.name, 'nsc') }}
run: sudo apt-get install -y --no-install-recommends libgles2 libegl1 run: sudo apt-get install -y --no-install-recommends libgles2 libegl1
- name: Restore scons cache
uses: actions/cache@v6
with:
path: /tmp/scons_cache
key: scons-${{ runner.os }}-${{ github.job }}-${{ github.sha }}
restore-keys: scons-${{ runner.os }}-${{ github.job }}-
- name: Build openpilot - name: Build openpilot
run: scons run: scons
- name: Create UI Report - name: Create UI Report
+1 -1
View File
@@ -170,6 +170,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}}, {"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}},
{"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}}, {"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}},
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}}, {"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
{"FollowDistanceAccelBoost", {PERSISTENT | BACKUP, BOOL, "0"}},
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}}, {"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
{"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}}, {"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}},
{"HasAcceptedTermsSP", {PERSISTENT, STRING, "0"}}, {"HasAcceptedTermsSP", {PERSISTENT, STRING, "0"}},
@@ -246,7 +247,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"TeslaMadsScreenButton", {PERSISTENT | BACKUP, INT, "0"}}, {"TeslaMadsScreenButton", {PERSISTENT | BACKUP, INT, "0"}},
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaTSS2Smooth", {PERSISTENT | BACKUP, BOOL, "0"}},
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}}, {"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -64,19 +64,16 @@ class DesireHelper:
((carstate.steeringTorque > 0 and self.lane_change_direction == LaneChangeDirection.left) or ((carstate.steeringTorque > 0 and self.lane_change_direction == LaneChangeDirection.left) or
(carstate.steeringTorque < 0 and self.lane_change_direction == LaneChangeDirection.right)) (carstate.steeringTorque < 0 and self.lane_change_direction == LaneChangeDirection.right))
blindspot_detected = ((carstate.leftBlindspot and self.lane_change_direction == LaneChangeDirection.left) or blindspot_detected = (((carstate.leftBlindspot or left_edge_detected) and self.lane_change_direction == LaneChangeDirection.left) or
(carstate.rightBlindspot and self.lane_change_direction == LaneChangeDirection.right)) ((carstate.rightBlindspot or right_edge_detected) and self.lane_change_direction == LaneChangeDirection.right))
edge_detected = ((left_edge_detected and self.lane_change_direction == LaneChangeDirection.left) or self.alc.update_lane_change(blindspot_detected, carstate.brakePressed)
(right_edge_detected and self.lane_change_direction == LaneChangeDirection.right))
self.alc.update_lane_change(blindspot_detected, edge_detected, carstate.brakePressed, torque_applied)
if not one_blinker or below_lane_change_speed: if not one_blinker or below_lane_change_speed:
self.lane_change_state = LaneChangeState.off self.lane_change_state = LaneChangeState.off
self.lane_change_direction = LaneChangeDirection.none self.lane_change_direction = LaneChangeDirection.none
self.lane_change_timer = 0.0 self.lane_change_timer = 0.0
elif (torque_applied or self.alc.auto_lane_change_allowed) and not self.alc.lane_change_blocked: elif (torque_applied or self.alc.auto_lane_change_allowed) and not blindspot_detected:
self.lane_change_state = LaneChangeState.laneChangeStarting self.lane_change_state = LaneChangeState.laneChangeStarting
self.lane_change_timer = 0.0 self.lane_change_timer = 0.0
@@ -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)
@@ -21,6 +21,9 @@ class TogglesLayoutMici(NavScroller):
"Use the steering wheel distance button on supported cars.") "Use the steering wheel distance button on supported cars.")
self._initial_cruise_speed = InitialCruiseSpeedButton() self._initial_cruise_speed = InitialCruiseSpeedButton()
self._initial_cruise_speed.set_visible(lambda: ui_state.CP is not None and ui_state.has_longitudinal_control and not ui_state.CP.pcmCruise) self._initial_cruise_speed.set_visible(lambda: ui_state.CP is not None and ui_state.has_longitudinal_control and not ui_state.CP.pcmCruise)
self._accel_boost_toggle = BigParamControl("extra acceleration boost", "FollowDistanceAccelBoost",
description="Adds extra acceleration on top of the stock gas-override boost.\n" +
"The extra boost follows Following Distance: strongest when Aggressive, weakest when Relaxed.")
is_metric_toggle = BigToggle("use metric units", initial_state=ui_state.params.get_bool("IsMetric"), toggle_callback=self._on_metric_toggle) is_metric_toggle = BigToggle("use metric units", initial_state=ui_state.params.get_bool("IsMetric"), toggle_callback=self._on_metric_toggle)
ldw_toggle = BigParamControl("lane departure warnings", "IsLdwEnabled", ldw_toggle = BigParamControl("lane departure warnings", "IsLdwEnabled",
description="Warn when you drift across a detected lane line.\n" + description="Warn when you drift across a detected lane line.\n" +
@@ -40,6 +43,7 @@ class TogglesLayoutMici(NavScroller):
self._scroller.add_widgets([ self._scroller.add_widgets([
self._personality_toggle, self._personality_toggle,
self._accel_boost_toggle,
self._initial_cruise_speed, self._initial_cruise_speed,
is_metric_toggle, is_metric_toggle,
ldw_toggle, ldw_toggle,
@@ -51,6 +55,7 @@ class TogglesLayoutMici(NavScroller):
# Toggle lists # Toggle lists
self._refresh_toggles = ( self._refresh_toggles = (
("FollowDistanceAccelBoost", self._accel_boost_toggle),
("IsMetric", is_metric_toggle), ("IsMetric", is_metric_toggle),
("IsLdwEnabled", ldw_toggle), ("IsLdwEnabled", ldw_toggle),
("AlwaysOnDM", always_on_dm_toggle), ("AlwaysOnDM", always_on_dm_toggle),
@@ -92,6 +97,7 @@ class TogglesLayoutMici(NavScroller):
if ui_state.CP is not None: if ui_state.CP is not None:
self._personality_toggle.set_visible(ui_state.has_longitudinal_control) self._personality_toggle.set_visible(ui_state.has_longitudinal_control)
self._accel_boost_toggle.set_visible(ui_state.has_longitudinal_control)
# Refresh toggles from params to mirror external changes # Refresh toggles from params to mirror external changes
for key, item in self._refresh_toggles: for key, item in self._refresh_toggles:
@@ -22,6 +22,7 @@ from openpilot.common.transformations.orientation import rot_from_euler
from enum import IntEnum from enum import IntEnum
if gui_app.sunnypilot_ui(): if gui_app.sunnypilot_ui():
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.accel_boost_indicator import AccelBoostIndicator
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.hud_renderer import HudRendererSP as HudRenderer from openpilot.selfdrive.ui.sunnypilot.mici.onroad.hud_renderer import HudRendererSP as HudRenderer
from openpilot.selfdrive.ui.sunnypilot.ui_state import OnroadTimerStatus from openpilot.selfdrive.ui.sunnypilot.ui_state import OnroadTimerStatus
@@ -181,6 +182,7 @@ class AugmentedRoadView(CameraView):
self._alert_renderer = AlertRenderer() self._alert_renderer = AlertRenderer()
self._driver_state_renderer = DriverStateRenderer() self._driver_state_renderer = DriverStateRenderer()
self._confidence_ball = ConfidenceBall() self._confidence_ball = ConfidenceBall()
self._accel_boost_indicator = AccelBoostIndicator() if gui_app.sunnypilot_ui() else None
self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY, self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY,
text_color=rl.Color(255, 255, 255, int(255 * 0.9)), text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
alignment=TextAlignment.CENTER, alignment=TextAlignment.CENTER,
@@ -275,7 +277,13 @@ class AugmentedRoadView(CameraView):
if show_navigation: if show_navigation:
self._navigation.render(rl.Rectangle(self._content_rect.x + self._content_rect.width, self.rect.y, PANEL_WIDTH, self.rect.height)) self._navigation.render(rl.Rectangle(self._content_rect.x + self._content_rect.width, self.rect.y, PANEL_WIDTH, self.rect.height))
else: else:
self._confidence_ball.render(self.rect) # The boost bar owns the side panel while the extra boost is on; the ball only shows with the feature off
if gui_app.sunnypilot_ui():
self._accel_boost_indicator.render(self.rect)
if not self._accel_boost_indicator.panel_in_use:
self._confidence_ball.render(self.rect)
else:
self._confidence_ball.render(self.rect)
self._bookmark_icon.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.") 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.") 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): class CruiseLayout(Widget):
def __init__(self): def __init__(self):
@@ -76,6 +81,11 @@ class CruiseLayout(Widget):
min_value=1, max_value=3, value_change_step=1, min_value=1, max_value=3, value_change_step=1,
inline=True) 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( self.sla_settings_button = simple_button_item_sp(
button_text=lambda: tr("Speed Limit"), button_text=lambda: tr("Speed Limit"),
button_width=800, button_width=800,
@@ -86,6 +96,7 @@ class CruiseLayout(Widget):
self.icbm_toggle, self.icbm_toggle,
self.scc_v_toggle, self.scc_v_toggle,
self.scc_m_toggle, self.scc_m_toggle,
self.accel_boost_toggle,
self.custom_acc_toggle, self.custom_acc_toggle,
self.custom_acc_short_increment, self.custom_acc_short_increment,
self.custom_acc_long_increment, self.custom_acc_long_increment,
@@ -104,6 +115,7 @@ class CruiseLayout(Widget):
self._scroller.show_event() self._scroller.show_event()
self.icbm_toggle.show_description(True) self.icbm_toggle.show_description(True)
self.custom_acc_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): def _set_current_panel(self, panel: PanelType):
self._current_panel = panel self._current_panel = panel
@@ -148,10 +160,17 @@ class CruiseLayout(Widget):
self.scc_v_toggle.action_item.set_enabled(False) self.scc_v_toggle.action_item.set_enabled(False)
self.scc_m_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)
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: else:
has_icbm = has_long = False has_icbm = has_long = False
self.icbm_toggle.action_item.set_enabled(False) self.icbm_toggle.action_item.set_enabled(False)
self.icbm_toggle.set_description(tr(ONROAD_ONLY_DESCRIPTION)) self.icbm_toggle.set_description(tr(ONROAD_ONLY_DESCRIPTION))
self.accel_boost_toggle.action_item.set_enabled(False)
show_custom_acc_desc = False show_custom_acc_desc = False
@@ -11,12 +11,10 @@ from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.widgets import DialogResult from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
from opendbc.car.toyota.values import ToyotaFlags
ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.") 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.") SNG_HACK_UNAVAILABLE = tr_noop("sunnypilot Longitudinal Control must be available and enabled for your vehicle to use this feature.")
PLATFORM_UNAVAILABLE = tr_noop("This feature is currently not available on this platform.")
DESCRIPTIONS = { DESCRIPTIONS = {
'enforce_stock_longitudinal': tr_noop( 'enforce_stock_longitudinal': tr_noop(
@@ -25,11 +23,7 @@ DESCRIPTIONS = {
'stop_and_go_hack': tr_noop( 'stop_and_go_hack': tr_noop(
'sunnypilot will allow some Toyota/Lexus cars to auto resume during stop and go traffic. ' + '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.' '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.'
), )
'tss2_smooth': tr_noop(
'Enable smoother longitudinal control for TSS2 vehicles. ' +
'Uses a custom speed-dependent integral gain curve for smoother traffic driving.'
),
} }
@@ -53,18 +47,9 @@ class ToyotaSettings(BrandSettings):
enabled=lambda: not ui_state.engaged, enabled=lambda: not ui_state.engaged,
) )
self.tss2_smooth = toggle_item_sp(
lambda: tr("TSS2 Smooth Longitudinal (Alpha)"),
description=lambda: tr(DESCRIPTIONS["tss2_smooth"]),
initial_state=ui_state.params.get_bool("ToyotaTSS2Smooth"),
callback=self._on_enable_tss2_smooth,
enabled=lambda: not ui_state.engaged,
)
self.items = [ self.items = [
self.enforce_stock_longitudinal, self.enforce_stock_longitudinal,
self.stop_and_go_hack, self.stop_and_go_hack,
self.tss2_smooth,
] ]
def _on_enable_enforce_stock_longitudinal(self, state: bool): def _on_enable_enforce_stock_longitudinal(self, state: bool):
@@ -76,8 +61,6 @@ class ToyotaSettings(BrandSettings):
ui_state.params.put_bool("AlphaLongitudinalEnabled", False) ui_state.params.put_bool("AlphaLongitudinalEnabled", False)
ui_state.params.put_bool("ToyotaStopAndGoHack", False) ui_state.params.put_bool("ToyotaStopAndGoHack", False)
self.stop_and_go_hack.action_item.set_state(False) self.stop_and_go_hack.action_item.set_state(False)
ui_state.params.put_bool("ToyotaTSS2Smooth", False)
self.tss2_smooth.action_item.set_state(False)
ui_state.params.put_bool("OnroadCycleRequested", True) ui_state.params.put_bool("OnroadCycleRequested", True)
else: else:
self.enforce_stock_longitudinal.action_item.set_state(False) self.enforce_stock_longitudinal.action_item.set_state(False)
@@ -92,25 +75,6 @@ class ToyotaSettings(BrandSettings):
ui_state.params.put_bool("ToyotaEnforceStockLongitudinal", False) ui_state.params.put_bool("ToyotaEnforceStockLongitudinal", False)
ui_state.params.put_bool("OnroadCycleRequested", True) ui_state.params.put_bool("OnroadCycleRequested", True)
def _on_enable_tss2_smooth(self, state: bool):
if state:
def confirm_callback(result: int):
if result == DialogResult.CONFIRM:
ui_state.params.put_bool("ToyotaTSS2Smooth", True)
ui_state.params.put_bool("OnroadCycleRequested", True)
else:
self.tss2_smooth.action_item.set_state(False)
content = (f"<h1>{self.tss2_smooth.title}</h1><br>" +
f"<p>{self.tss2_smooth.description}</p>")
dlg = ConfirmDialog(content, tr("Enable"), rich=True, callback=confirm_callback)
gui_app.push_widget(dlg)
else:
ui_state.params.put_bool("ToyotaTSS2Smooth", False)
ui_state.params.put_bool("OnroadCycleRequested", True)
def _on_enable_stop_and_go_hack(self, state: bool): def _on_enable_stop_and_go_hack(self, state: bool):
if state: if state:
def confirm_callback(result: int): def confirm_callback(result: int):
@@ -149,32 +113,9 @@ class ToyotaSettings(BrandSettings):
self.stop_and_go_hack.set_description(new_desc) self.stop_and_go_hack.set_description(new_desc)
if show_desc: if show_desc:
self.stop_and_go_hack.show_description(True) self.stop_and_go_hack.show_description(True)
tss2 = bool(ui_state.CP.flags & ToyotaFlags.TSS2)
if longitudinal and not enforce_stock and tss2:
self.tss2_smooth.action_item.set_enabled(not ui_state.engaged)
new_desc = tr(DESCRIPTIONS["tss2_smooth"])
show_desc = False
else:
self.tss2_smooth.action_item.set_enabled(False)
self.tss2_smooth.action_item.set_state(False)
unavailable = SNG_HACK_UNAVAILABLE if tss2 else PLATFORM_UNAVAILABLE
new_desc = "<b>" + tr(unavailable) + "</b>\n\n" + tr(DESCRIPTIONS["tss2_smooth"])
show_desc = True
if self.tss2_smooth.description != new_desc:
self.tss2_smooth.set_description(new_desc)
if show_desc:
self.tss2_smooth.show_description(True)
else: else:
self.stop_and_go_hack.action_item.set_enabled(False) 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"]) 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: if self.stop_and_go_hack.description != new_desc:
self.stop_and_go_hack.set_description(new_desc) self.stop_and_go_hack.set_description(new_desc)
self.stop_and_go_hack.show_description(True) self.stop_and_go_hack.show_description(True)
self.tss2_smooth.action_item.set_enabled(False)
new_desc = "<b>" + tr(ONROAD_ONLY_DESCRIPTION) + "</b>\n\n" + tr(DESCRIPTIONS["tss2_smooth"])
if self.tss2_smooth.description != new_desc:
self.tss2_smooth.set_description(new_desc)
self.tss2_smooth.show_description(True)
@@ -0,0 +1,122 @@
"""
Copyright (c) 2021-, 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.
"""
import pyray as rl
from openpilot.cereal import log
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 gui_app
from openpilot.system.ui.widgets import Widget
from openpilot.sunnypilot.selfdrive.controls.lib.accel_boost_ext import get_accel_boost_max
# The readout is wider than the side panel, so the value is right-aligned and bleeds over the camera view
BAR_WIDTH = 24
# Fades to white only near the profile cap, like the stock torque bar fading toward orange at high torque
CAP_BLEND_START = 0.75
MAX_BOOST_BLEND = 0.45
COLOR_TRACK = rl.Color(255, 255, 255, 45)
COLOR_TRACK_EDGE = rl.Color(255, 255, 255, 70)
AGGRESSIVE_COLOR = rl.Color(255, 80, 60, 255)
STANDARD_COLOR = rl.Color(0, 230, 120, 255)
RELAXED_COLOR = rl.Color(70, 150, 255, 255)
FADE_THRESHOLD = 0.01
def lighten(color: rl.Color, amount: float) -> rl.Color:
"""Lerp towards white in RGB, so the hue stays put the way HSV blending would not."""
amount = min(max(amount, 0.0), 1.0)
return rl.Color(int(color.r + (255 - color.r) * amount),
int(color.g + (255 - color.g) * amount),
int(color.b + (255 - color.b) * amount), color.a)
def get_boost_fraction(boost: float, max_boost: float) -> float:
"""How much of the active profile's boost budget is in use, clamped to a full bar."""
if max_boost <= 0.0:
return 0.0
return min(max(boost / max_boost, 0.0), 1.0)
def get_boost_style(personality) -> tuple[rl.Color, float]:
if personality == log.LongitudinalPersonality.aggressive:
color = AGGRESSIVE_COLOR
elif personality == log.LongitudinalPersonality.relaxed:
color = RELAXED_COLOR
else:
color = STANDARD_COLOR
return color, get_accel_boost_max(personality)
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)
# Smoothly rescale the bar when the follow distance profile changes mid-drive
self._max_boost_filter = FirstOrderFilter(0.0, 0.25, 1 / gui_app.target_fps)
self._profile_color = STANDARD_COLOR
self._max_boost = 0.0
self.panel_in_use = False
def _update_state(self):
sm = ui_state.sm
extra_enabled = ui_state.follow_distance_accel_boost
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 and extra_enabled else 0.0)
self._boost_filter.update(float(sm['longitudinalPlan'].accelBoost) if visible else 0.0)
if sm.alive['selfdriveState']:
self._profile_color, self._max_boost = get_boost_style(sm['selfdriveState'].personality)
self._max_boost_filter.update(self._max_boost)
# The bar owns the panel only while it is actually drawn; the ball returns when the bar fades out
self.panel_in_use = visible and extra_enabled
def _render(self, _):
alpha = self._alpha_filter.x
if alpha < FADE_THRESHOLD or self._max_boost <= 0.0:
return
cx = self.rect.x + self.rect.width - SIDE_PANEL_WIDTH / 2
bar_x = cx - BAR_WIDTH / 2
track_top = self.rect.y + self.rect.height * 0.08
track_bottom = self.rect.y + self.rect.height * 0.92
track_h = max(1.0, track_bottom - track_top)
# track, light enough to read on the black side panel
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)))
rl.draw_rectangle_rounded_lines_ex(rl.Rectangle(bar_x, track_top, BAR_WIDTH, track_h), 0.5, 12, 1,
rl.Color(COLOR_TRACK_EDGE.r, COLOR_TRACK_EDGE.g, COLOR_TRACK_EDGE.b,
int(COLOR_TRACK_EDGE.a * alpha)))
# Scale to the current profile's cap, so a maxed boost fills the bar and a profile change rescales it
boost = max(self._boost_filter.x, 0.0)
ratio = get_boost_fraction(boost, self._max_boost_filter.x)
fill_h = ratio * track_h
cap_blend = max(0.0, (ratio - CAP_BLEND_START) / (1.0 - CAP_BLEND_START))
fill_color = lighten(self._profile_color, MAX_BOOST_BLEND * cap_blend)
if fill_h > 0.0:
# soft glow around the tip, like the confidence ball's gradient
tip_y = track_bottom - fill_h
glow = rl.Color(fill_color.r, fill_color.g, fill_color.b, int(70 * alpha))
rl.draw_circle_gradient(rl.Vector2(cx, tip_y), int(BAR_WIDTH * 0.9), glow, rl.BLANK)
fill = rl.Color(fill_color.r, fill_color.g, fill_color.b, int(255 * alpha))
if fill_h < BAR_WIDTH:
# Keep the cap inside the track: a fixed radius would paint below the track near zero boost
radius = max(1, int(fill_h / 2))
rl.draw_circle(int(cx), int(track_bottom - radius), radius, fill)
else:
rl.draw_rectangle_rounded(rl.Rectangle(bar_x, track_bottom - fill_h, BAR_WIDTH, fill_h), 0.5, 12, fill)
@@ -0,0 +1,59 @@
"""
Copyright (c) 2021-, 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.
"""
import unittest
from openpilot.cereal import log
from openpilot.selfdrive.controls.lib.accel_boost import ACCEL_BOOST_MAX
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.accel_boost_indicator import (get_boost_style, get_boost_fraction, lighten,
AGGRESSIVE_COLOR, RELAXED_COLOR, STANDARD_COLOR)
from openpilot.sunnypilot.selfdrive.controls.lib.accel_boost_ext import (SCALE_AGGRESSIVE, SCALE_RELAXED, SCALE_STANDARD,
get_accel_boost_max)
class TestAccelBoostIndicator(unittest.TestCase):
def test_bar_scales_with_following_distance(self):
for personality, color, scale in (
(log.LongitudinalPersonality.relaxed, RELAXED_COLOR, SCALE_RELAXED),
(log.LongitudinalPersonality.standard, STANDARD_COLOR, SCALE_STANDARD),
(log.LongitudinalPersonality.aggressive, AGGRESSIVE_COLOR, SCALE_AGGRESSIVE)):
bar_color, full_scale = get_boost_style(personality)
self.assertEqual(bar_color, color)
self.assertAlmostEqual(full_scale, ACCEL_BOOST_MAX * (1.0 + scale))
def test_bar_fills_at_the_active_profile_cap(self):
caps = [get_accel_boost_max(p) for p in (log.LongitudinalPersonality.relaxed,
log.LongitudinalPersonality.standard,
log.LongitudinalPersonality.aggressive)]
self.assertEqual(caps, sorted(caps))
for cap in caps:
self.assertAlmostEqual(get_boost_fraction(cap, cap), 1.0)
self.assertAlmostEqual(get_boost_fraction(0.0, cap), 0.0)
self.assertAlmostEqual(get_boost_fraction(cap * 0.5, cap), 0.5)
# A boost beyond the current cap (e.g. after switching to a shorter profile) still fills the bar
self.assertAlmostEqual(get_boost_fraction(caps[2], caps[0]), 1.0)
self.assertAlmostEqual(get_boost_fraction(0.0, 0.0), 0.0)
def test_cap_lighten_keeps_the_profile_hue(self):
# An HSV blend towards white sweeps relaxed blue through pink, so the fill lightens in RGB instead
relaxed = lighten(RELAXED_COLOR, 0.45)
aggressive = lighten(AGGRESSIVE_COLOR, 0.45)
self.assertGreater(relaxed.b, relaxed.r)
self.assertGreaterEqual(relaxed.b, relaxed.g)
self.assertGreater(aggressive.r, aggressive.b)
# lighter in RGB means the dimmest channel came up towards white
self.assertGreater(relaxed.r, RELAXED_COLOR.r)
self.assertGreater(aggressive.g, AGGRESSIVE_COLOR.g)
def test_wire_personality_picks_the_right_profile(self):
# Over the wire personality is a capnp DynamicEnum, which compares equal to the cereal enum but hashes differently
msg = log.SelfdriveState.new_message()
msg.personality = log.LongitudinalPersonality.aggressive
color, full_scale = get_boost_style(msg.personality)
self.assertEqual(color, AGGRESSIVE_COLOR)
self.assertAlmostEqual(full_scale, get_accel_boost_max(log.LongitudinalPersonality.aggressive))
@@ -64,6 +64,7 @@ class UIStateSP:
self.enforce_torque_control: bool = False self.enforce_torque_control: bool = False
self.custom_torque_params: bool = False self.custom_torque_params: bool = False
self.torque_override_enabled: bool = False self.torque_override_enabled: bool = False
self.follow_distance_accel_boost: bool = False
self._sp_initialized: bool = False self._sp_initialized: bool = False
def update(self) -> None: def update(self) -> None:
@@ -177,6 +178,7 @@ class UIStateSP:
self.enforce_torque_control = self.params.get_bool("EnforceTorqueControl") self.enforce_torque_control = self.params.get_bool("EnforceTorqueControl")
self.custom_torque_params = self.params.get_bool("CustomTorqueParams") self.custom_torque_params = self.params.get_bool("CustomTorqueParams")
self.torque_override_enabled = self.params.get_bool("TorqueParamsOverrideEnabled") self.torque_override_enabled = self.params.get_bool("TorqueParamsOverrideEnabled")
self.follow_distance_accel_boost = self.params.get_bool("FollowDistanceAccelBoost")
self.torque_override_lat_accel_factor = float(self.params.get("TorqueParamsOverrideLatAccelFactor", return_default=True)) self.torque_override_lat_accel_factor = float(self.params.get("TorqueParamsOverrideLatAccelFactor", return_default=True))
self.torque_override_friction = float(self.params.get("TorqueParamsOverrideFriction", return_default=True)) self.torque_override_friction = float(self.params.get("TorqueParamsOverrideFriction", return_default=True))
self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI") self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI")
@@ -231,6 +233,10 @@ class UIStateSP:
self.params.remove("IntelligentCruiseButtonManagement") self.params.remove("IntelligentCruiseButtonManagement")
self.has_icbm = False self.has_icbm = False
# Extra boost needs sunnypilot longitudinal control; ICBM alone is not enough
if not has_long:
self.params.remove("FollowDistanceAccelBoost")
# Cruise features requiring longitudinal or ICBM # Cruise features requiring longitudinal or ICBM
if not (has_long or self.has_icbm): if not (has_long or self.has_icbm):
self.params.remove("CustomAccIncrementsEnabled") self.params.remove("CustomAccIncrementsEnabled")
@@ -126,7 +126,6 @@ def initialize_params(params) -> list[dict[str, Any]]:
keys.extend([ keys.extend([
"ToyotaEnforceStockLongitudinal", "ToyotaEnforceStockLongitudinal",
"ToyotaStopAndGoHack", "ToyotaStopAndGoHack",
"ToyotaTSS2Smooth",
]) ])
return [{k: params.get(k, return_default=True)} for k in keys] return [{k: params.get(k, return_default=True)} for k in keys]
@@ -0,0 +1,65 @@
"""
Copyright (c) 2021-, 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:
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.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
return self._stock.update(sm, output_a_target_e2e, output_a_target_mpc, a_cruise)
target = output_a_target_e2e + self.extra_boost
return self._stock.update(sm, target, output_a_target_mpc, a_cruise)
@@ -10,8 +10,6 @@ from openpilot.common.params import Params
from openpilot.common.realtime import DT_MDL from openpilot.common.realtime import DT_MDL
class AutoLaneChangeMode: class AutoLaneChangeMode:
OFF = -1 OFF = -1
NUDGE = 0 # default NUDGE = 0 # default
@@ -33,7 +31,6 @@ AUTO_LANE_CHANGE_TIMER = {
} }
ONE_SECOND_DELAY = -1 ONE_SECOND_DELAY = -1
EDGE_DELAY = -2
class AutoLaneChangeController: class AutoLaneChangeController:
@@ -51,7 +48,6 @@ class AutoLaneChangeController:
self.prev_brake_pressed = False self.prev_brake_pressed = False
self.auto_lane_change_allowed = False self.auto_lane_change_allowed = False
self.prev_lane_change = False self.prev_lane_change = False
self.lane_change_blocked = False
self.read_params() self.read_params()
@@ -72,7 +68,7 @@ class AutoLaneChangeController:
self.read_params() self.read_params()
self.param_read_counter += 1 self.param_read_counter += 1
def update_lane_change_timers(self, blindspot_detected: bool, edge_detected: bool) -> None: def update_lane_change_timers(self, blindspot_detected: bool) -> None:
self.lane_change_delay = AUTO_LANE_CHANGE_TIMER.get(self.lane_change_set_timer, self.lane_change_delay = AUTO_LANE_CHANGE_TIMER.get(self.lane_change_set_timer,
AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.NUDGE]) AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.NUDGE])
@@ -84,12 +80,6 @@ class AutoLaneChangeController:
else: else:
self.lane_change_wait_timer = self.lane_change_delay + ONE_SECOND_DELAY self.lane_change_wait_timer = self.lane_change_delay + ONE_SECOND_DELAY
if edge_detected and self.lane_change_delay > 0:
if self.lane_change_delay == AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.NUDGELESS]:
self.lane_change_wait_timer = min(self.lane_change_wait_timer, EDGE_DELAY)
else:
self.lane_change_wait_timer = min(self.lane_change_wait_timer, self.lane_change_delay + EDGE_DELAY)
def update_allowed(self) -> bool: def update_allowed(self) -> bool:
# Auto lane change allowed if: # Auto lane change allowed if:
# 1. A valid delay is set (non-zero) # 1. A valid delay is set (non-zero)
@@ -107,23 +97,14 @@ class AutoLaneChangeController:
return bool(self.lane_change_wait_timer > self.lane_change_delay) return bool(self.lane_change_wait_timer > self.lane_change_delay)
def update_lane_change(self, blindspot_detected: bool, edge_detected: bool, brake_pressed: bool, def update_lane_change(self, blindspot_detected: bool, brake_pressed: bool) -> None:
torque_applied: bool = False) -> None:
if brake_pressed and not self.prev_brake_pressed: if brake_pressed and not self.prev_brake_pressed:
self.prev_brake_pressed = brake_pressed self.prev_brake_pressed = brake_pressed
self.update_lane_change_timers(blindspot_detected, edge_detected) self.update_lane_change_timers(blindspot_detected)
self.auto_lane_change_allowed = self.update_allowed() self.auto_lane_change_allowed = self.update_allowed()
# BSM always blocks. Edge blocks auto mode but nudge can override.
if blindspot_detected:
self.lane_change_blocked = True
elif edge_detected:
self.lane_change_blocked = not torque_applied
else:
self.lane_change_blocked = False
def update_state(self): def update_state(self):
if self.DH.lane_change_state == log.LaneChangeState.laneChangeStarting: if self.DH.lane_change_state == log.LaneChangeState.laneChangeStarting:
self.prev_lane_change = True self.prev_lane_change = True
@@ -9,6 +9,7 @@ from openpilot.cereal import messaging, custom
from opendbc.car import structs from opendbc.car import structs
from openpilot.common.constants import CV from openpilot.common.constants import CV
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX 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.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.smart_cruise_control.smart_cruise_control import SmartCruiseControl
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist 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_v_target = 0.
self.output_a_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]: def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]:
CS = sm['carState'] CS = sm['carState']
@@ -0,0 +1,242 @@
"""
Copyright (c) 2021-, 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 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, ACCEL_BOOST_RATE
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 / (ACCEL_BOOST_RATE * 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.total_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.total_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.total_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.total_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.total_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.total_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])
@@ -8,7 +8,7 @@ from openpilot.common.parameterized import parameterized
from openpilot.common.realtime import DT_MDL from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper, LaneChangeState, LaneChangeDirection from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper, LaneChangeState, LaneChangeDirection
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode, \ from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode, \
AUTO_LANE_CHANGE_TIMER, ONE_SECOND_DELAY, EDGE_DELAY AUTO_LANE_CHANGE_TIMER, ONE_SECOND_DELAY
from openpilot.common.test import OpenpilotTestCase from openpilot.common.test import OpenpilotTestCase
AUTO_LANE_CHANGE_TIMER_COMBOS = [ AUTO_LANE_CHANGE_TIMER_COMBOS = [
@@ -61,7 +61,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
# Update controller # Update controller
num_updates = int(5.0 / DT_MDL) num_updates = int(5.0 / DT_MDL)
for _ in range(num_updates): # Run for 5 seconds for _ in range(num_updates): # Run for 5 seconds
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False) self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
# Mode should not allow lane change immediately # Mode should not allow lane change immediately
assert not self.alc.auto_lane_change_allowed assert not self.alc.auto_lane_change_allowed
@@ -74,11 +74,11 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
self.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGELESS self.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGELESS
# Update controller once to read params # Update controller once to read params
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False) self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
# Update multiple times to exceed the timer threshold # Update multiple times to exceed the timer threshold
for _ in range(1): # Should exceed 0.1s with multiple DT_MDL updates for _ in range(1): # Should exceed 0.1s with multiple DT_MDL updates
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False) self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
# Now lane change should be allowed # Now lane change should be allowed
assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay
@@ -91,7 +91,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
self.alc.lane_change_set_timer = timer_state self.alc.lane_change_set_timer = timer_state
# Update controller once # Update controller once
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False) self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
# The timer should still be below the threshold after one update # The timer should still be below the threshold after one update
assert not self.alc.auto_lane_change_allowed assert not self.alc.auto_lane_change_allowed
@@ -99,7 +99,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
# Update enough times to exceed the threshold (seconds / DT_MDL) # Update enough times to exceed the threshold (seconds / DT_MDL)
num_updates = int(timer_delay / DT_MDL) + 1 # Add one extra updates to ensure we exceed the threshold num_updates = int(timer_delay / DT_MDL) + 1 # Add one extra updates to ensure we exceed the threshold
for _ in range(num_updates): for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False) self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
# Now lane change should be allowed # Now lane change should be allowed
assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay
@@ -116,7 +116,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
# Update with brake pressed for 1 second # Update with brake pressed for 1 second
for _ in range(num_updates): for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=True) self.alc.update_lane_change(blindspot_detected=False, brake_pressed=True)
# Even though it is an auto lane change mode, lane change should be disallowed due to brake pressed prior initiating lane change # Even though it is an auto lane change mode, lane change should be disallowed due to brake pressed prior initiating lane change
assert not self.alc.auto_lane_change_allowed assert not self.alc.auto_lane_change_allowed
@@ -126,7 +126,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
# Even releasing brake shouldn't allow auto lane change # Even releasing brake shouldn't allow auto lane change
for _ in range(num_updates): for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False) self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
assert not self.alc.auto_lane_change_allowed assert not self.alc.auto_lane_change_allowed
@@ -139,13 +139,13 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
self.alc.lane_change_set_timer = timer_state self.alc.lane_change_set_timer = timer_state
# Update with blindspot detected - this should prevent auto lane change # Update with blindspot detected - this should prevent auto lane change
self.alc.update_lane_change(blindspot_detected=True, edge_detected=False, brake_pressed=False) self.alc.update_lane_change(blindspot_detected=True, brake_pressed=False)
assert not self.alc.auto_lane_change_allowed assert not self.alc.auto_lane_change_allowed
# Keep updating with blindspot detected - should still prevent auto lane change # Keep updating with blindspot detected - should still prevent auto lane change
num_updates = int(timer_delay / DT_MDL) + 1 # Add one extra updates to ensure we exceed the threshold num_updates = int(timer_delay / DT_MDL) + 1 # Add one extra updates to ensure we exceed the threshold
for _ in range(num_updates): for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=True, edge_detected=False, brake_pressed=False) self.alc.update_lane_change(blindspot_detected=True, brake_pressed=False)
assert not self.alc.auto_lane_change_allowed assert not self.alc.auto_lane_change_allowed
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS) @parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
@@ -157,13 +157,13 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
self.alc.lane_change_set_timer = timer_state self.alc.lane_change_set_timer = timer_state
# First update with blindspot detected to set the negative timer # First update with blindspot detected to set the negative timer
self.alc.update_lane_change(blindspot_detected=True, edge_detected=False, brake_pressed=False) self.alc.update_lane_change(blindspot_detected=True, brake_pressed=False)
assert not self.alc.auto_lane_change_allowed assert not self.alc.auto_lane_change_allowed
# Now update with blindspot cleared - should start incrementing timer from negative value # Now update with blindspot cleared - should start incrementing timer from negative value
num_updates = int((timer_delay + abs(ONE_SECOND_DELAY)) / DT_MDL) + 1 num_updates = int((timer_delay + abs(ONE_SECOND_DELAY)) / DT_MDL) + 1
for _ in range(num_updates): for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False) self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
# After sufficient updates with no blindspot, auto lane change should be allowed # After sufficient updates with no blindspot, auto lane change should be allowed
assert self.alc.auto_lane_change_allowed assert self.alc.auto_lane_change_allowed
@@ -177,7 +177,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
# Update enough times to exceed the threshold (seconds / DT_MDL) # Update enough times to exceed the threshold (seconds / DT_MDL)
for _ in range(num_updates): for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False) self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
# Now lane change should be allowed # Now lane change should be allowed
assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay
@@ -193,7 +193,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
# Update enough times to exceed the threshold (seconds / DT_MDL) # Update enough times to exceed the threshold (seconds / DT_MDL)
for _ in range(num_updates): for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False) self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
assert not self.alc.auto_lane_change_allowed assert not self.alc.auto_lane_change_allowed
@@ -206,86 +206,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
# Simulate updates for a long period of time (e.g., 10 seconds) # Simulate updates for a long period of time (e.g., 10 seconds)
num_updates = int(10.0 / DT_MDL) num_updates = int(10.0 / DT_MDL)
for _ in range(num_updates): for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False) self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
# Lane change should never be allowed # Lane change should never be allowed
assert not self.alc.auto_lane_change_allowed assert not self.alc.auto_lane_change_allowed
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
def test_edge_detected_delays_auto_lane_change(self, timer_state, timer_delay):
"""Test that road edge detection adds delay to auto lane change."""
self._reset_states()
self.alc.lane_change_bsm_delay = False
self.alc.lane_change_set_timer = timer_state
# Update with edge detected - should prevent auto lane change
self.alc.update_lane_change(blindspot_detected=False, edge_detected=True, brake_pressed=False)
assert not self.alc.auto_lane_change_allowed
# Keep updating with edge detected - timer keeps resetting, should stay disallowed
num_updates = int(timer_delay / DT_MDL) + 1
for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, edge_detected=True, brake_pressed=False)
assert not self.alc.auto_lane_change_allowed
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
def test_edge_detected_then_cleared_allows_after_delay(self, timer_state, timer_delay):
"""Test that auto lane change is allowed after edge clears plus extra delay."""
self._reset_states()
self.alc.lane_change_bsm_delay = False
self.alc.lane_change_set_timer = timer_state
# First update with edge detected to set the negative timer
self.alc.update_lane_change(blindspot_detected=False, edge_detected=True, brake_pressed=False)
assert not self.alc.auto_lane_change_allowed
# Now update with edge cleared - should start incrementing timer from negative value
num_updates = int((timer_delay + abs(EDGE_DELAY)) / DT_MDL) + 1
for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
# After sufficient updates with no edge, auto lane change should be allowed
assert self.alc.auto_lane_change_allowed
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
def test_edge_and_bsm_combined_uses_worst_delay(self, timer_state, timer_delay):
"""Test that when both BSM and edge are detected, the worst (longest) delay wins."""
self._reset_states()
self.alc.lane_change_bsm_delay = True
self.alc.lane_change_set_timer = timer_state
# Update with both blindspot and edge detected
self.alc.update_lane_change(blindspot_detected=True, edge_detected=True, brake_pressed=False)
assert not self.alc.auto_lane_change_allowed
# Clear both - need to wait out the worst delay
worst_delay = max(abs(ONE_SECOND_DELAY), abs(EDGE_DELAY))
num_updates = int((timer_delay + worst_delay) / DT_MDL) + 1
for _ in range(num_updates):
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
assert self.alc.auto_lane_change_allowed
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
def test_nudge_overrides_edge_in_lane_change_blocked(self, timer_state, timer_delay):
"""Test that lane_change_blocked is False when nudge applied with edge-only."""
self._reset_states()
self.alc.lane_change_set_timer = timer_state
# Edge detected, no nudge - should be blocked
self.alc.update_lane_change(blindspot_detected=False, edge_detected=True, brake_pressed=False, torque_applied=False)
assert self.alc.lane_change_blocked
# Edge detected, nudge applied - should NOT be blocked
self.alc.update_lane_change(blindspot_detected=False, edge_detected=True, brake_pressed=False, torque_applied=True)
assert not self.alc.lane_change_blocked
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
def test_nudge_does_not_override_bsm_in_lane_change_blocked(self, timer_state, timer_delay):
"""Test that lane_change_blocked stays True when BSM detected even with nudge."""
self._reset_states()
self.alc.lane_change_set_timer = timer_state
# BSM detected with nudge - should still be blocked
self.alc.update_lane_change(blindspot_detected=True, edge_detected=False, brake_pressed=False, torque_applied=True)
assert self.alc.lane_change_blocked
@@ -113,56 +113,12 @@ class TestDesireHelperIntegration(OpenpilotTestCase):
left_edge_detected=False, right_edge_detected=False) left_edge_detected=False, right_edge_detected=False)
assert dh.desire == expected_desire assert dh.desire == expected_desire
def test_nudge_overrides_edge_detection(self, set_lane_turn_params): def test_edge_blocks_lane_change(self, set_lane_turn_params):
"""Manual steering nudge should override road edge detection and allow lane change."""
dh = DesireHelper() dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
carstate = DummyCarState(vEgo=15, leftBlinker=True, steeringPressed=True, steeringTorque=1) carstate = DummyCarState(vEgo=15, leftBlinker=True, steeringPressed=True, steeringTorque=1)
for _ in range(10): for _ in range(10):
dh.update(carstate, True, 1.0, left_edge_detected=True, right_edge_detected=False) dh.update(carstate, True, 1.0, left_edge_detected=True, right_edge_detected=False)
assert dh.lane_change_state == LaneChangeState.laneChangeStarting assert dh.lane_change_state == LaneChangeState.preLaneChange
assert dh.lane_change_direction == LaneChangeDirection.left assert dh.lane_change_direction == LaneChangeDirection.left
assert dh.desire == log.Desire.laneChangeLeft
def test_nudge_does_not_override_bsm(self, set_lane_turn_params):
"""Manual steering nudge must NOT override BSM blindspot detection."""
dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
carstate = DummyCarState(vEgo=15, leftBlinker=True, leftBlindspot=True, steeringPressed=True, steeringTorque=1)
for _ in range(10):
dh.update(carstate, True, 1.0, left_edge_detected=False, right_edge_detected=False)
assert dh.lane_change_state == LaneChangeState.preLaneChange
assert dh.desire == log.Desire.none assert dh.desire == log.Desire.none
def test_edge_blocks_auto_lane_change(self, set_lane_turn_params):
"""Road edge detection should still block auto (nudgeless) lane changes."""
dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGELESS
dh.alc.param_read_counter = 1
carstate = DummyCarState(vEgo=15, leftBlinker=True)
for _ in range(10):
dh.update(carstate, True, 1.0, left_edge_detected=True, right_edge_detected=False)
assert dh.lane_change_state == LaneChangeState.preLaneChange
assert dh.desire == log.Desire.none
def test_auto_lane_change_allowed_without_edge(self, set_lane_turn_params):
"""Auto lane change should proceed when no edge and no BSM detected."""
dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGELESS
dh.alc.param_read_counter = 1
carstate = DummyCarState(vEgo=15, leftBlinker=True)
for _ in range(10):
dh.update(carstate, True, 1.0, left_edge_detected=False, right_edge_detected=False)
assert dh.lane_change_state == LaneChangeState.laneChangeStarting
assert dh.desire == log.Desire.laneChangeLeft
def test_right_edge_does_not_block_left_lane_change(self, set_lane_turn_params):
"""Edge on opposite side should not affect lane change direction."""
dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGELESS
dh.alc.param_read_counter = 1
carstate = DummyCarState(vEgo=15, leftBlinker=True)
for _ in range(10):
dh.update(carstate, True, 1.0, left_edge_detected=False, right_edge_detected=True)
assert dh.lane_change_state == LaneChangeState.laneChangeStarting
assert dh.desire == log.Desire.laneChangeLeft
@@ -10,7 +10,6 @@ from openpilot.cereal import custom, messaging
from opendbc.car.structs import car from opendbc.car.structs import car
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, UNSUPPORTED_LONGITUDINAL_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.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.interfaces import has_bsm
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
from openpilot.common.params import Params from openpilot.common.params import Params
@@ -44,7 +43,6 @@ CAPABILITY_FIELDS = (
"device_type", "device_type",
"subaru_has_sng", "subaru_has_sng",
"hyundai_alpha_long_available", "hyundai_alpha_long_available",
"toyota_tss2",
) )
CAPABILITY_LABELS: dict[str, str] = { CAPABILITY_LABELS: dict[str, str] = {
@@ -67,7 +65,6 @@ CAPABILITY_LABELS: dict[str, str] = {
"device_type": "Device type", "device_type": "Device type",
"subaru_has_sng": "Subaru Stop-and-Go available", "subaru_has_sng": "Subaru Stop-and-Go available",
"hyundai_alpha_long_available": "Hyundai Alpha Longitudinal available", "hyundai_alpha_long_available": "Hyundai Alpha Longitudinal available",
"toyota_tss2": "Toyota TSS2 platform",
} }
# Explicit defaults for non-boolean capability fields # Explicit defaults for non-boolean capability fields
@@ -114,15 +111,6 @@ 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["subaru_has_sng"] = not bool(CP.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
caps["has_stop_and_go"] = caps["subaru_has_sng"] caps["has_stop_and_go"] = caps["subaru_has_sng"]
elif brand == "toyota":
if bundle_platform:
try:
caps["toyota_tss2"] = bool(TOYOTA_CAR[bundle_platform].config.flags & ToyotaFlags.TSS2)
except KeyError:
cloudlog.exception(f"capabilities: unknown toyota platform {bundle_platform!r}")
elif CP is not None:
caps["toyota_tss2"] = bool(CP.flags & ToyotaFlags.TSS2)
def generate_capabilities(params: Params | None = None) -> dict: def generate_capabilities(params: Params | None = None) -> dict:
"""Generate a SettingsCapabilities dict from CarParams + boolean params. """Generate a SettingsCapabilities dict from CarParams + boolean params.
+13 -27
View File
@@ -620,6 +620,19 @@
} }
] ]
}, },
{
"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": "capability",
"field": "has_longitudinal_control",
"equals": true
}
]
},
{ {
"key": "IntelligentCruiseButtonManagement", "key": "IntelligentCruiseButtonManagement",
"widget": "toggle", "widget": "toggle",
@@ -2324,33 +2337,6 @@
"equals": false "equals": false
} }
] ]
},
{
"key": "ToyotaTSS2Smooth",
"widget": "toggle",
"needs_onroad_cycle": true,
"title": "TSS2 Smooth Longitudinal (Alpha)",
"description": "Enable smoother longitudinal control for TSS2 vehicles. Uses a custom speed-dependent integral gain curve for smoother traffic driving.",
"enablement": [
{
"type": "not_engaged"
},
{
"type": "capability",
"field": "has_longitudinal_control",
"equals": true
},
{
"type": "param",
"key": "ToyotaEnforceStockLongitudinal",
"equals": false
},
{
"type": "capability",
"field": "toyota_tss2",
"equals": true
}
]
} }
] ]
} }
@@ -30,6 +30,14 @@ sections:
label: Relaxed label: Relaxed
enablement: enablement:
- $ref: '#/macros/longitudinal' - $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/longitudinal'
- key: IntelligentCruiseButtonManagement - key: IntelligentCruiseButtonManagement
widget: toggle widget: toggle
title: Intelligent Cruise Button Management (ICBM) (Alpha) title: Intelligent Cruise Button Management (ICBM) (Alpha)
@@ -101,18 +101,3 @@ sections:
- type: param - type: param
key: ToyotaEnforceStockLongitudinal key: ToyotaEnforceStockLongitudinal
equals: false equals: false
- key: ToyotaTSS2Smooth
widget: toggle
needs_onroad_cycle: true
title: TSS2 Smooth Longitudinal (Alpha)
description: Enable smoother longitudinal control for TSS2 vehicles. Uses a custom speed-dependent integral gain curve
for smoother traffic driving.
enablement:
- $ref: '#/macros/not_engaged'
- $ref: '#/macros/longitudinal'
- type: param
key: ToyotaEnforceStockLongitudinal
equals: false
- type: capability
field: toyota_tss2
equals: true
@@ -75,12 +75,6 @@ class TestOpaquePerBrandFlags(OpenpilotTestCase):
def test_hyundai_alpha_long_available_default_false(self, caps): def test_hyundai_alpha_long_available_default_false(self, caps):
assert caps["hyundai_alpha_long_available"] is False assert caps["hyundai_alpha_long_available"] is False
def test_toyota_tss2_field_present(self):
assert "toyota_tss2" in CAPABILITY_FIELDS
def test_toyota_tss2_default_false(self, caps):
assert caps["toyota_tss2"] is False
class TestCapabilitiesShape(OpenpilotTestCase): class TestCapabilitiesShape(OpenpilotTestCase):
def test_all_fields_present(self, caps): def test_all_fields_present(self, caps):
@@ -207,7 +207,6 @@ class TestNotEngagedReplacement(OpenpilotTestCase):
"AlphaLongitudinalEnabled", "AlphaLongitudinalEnabled",
"ToyotaEnforceStockLongitudinal", "ToyotaEnforceStockLongitudinal",
"ToyotaStopAndGoHack", "ToyotaStopAndGoHack",
"ToyotaTSS2Smooth",
], names=["key"]) ], names=["key"])
def test_offroad_only_replaced_with_not_engaged(self, schema, key): def test_offroad_only_replaced_with_not_engaged(self, schema, key):
"""These items should use not_engaged, not offroad_only.""" """These items should use not_engaged, not offroad_only."""
@@ -286,7 +286,6 @@ class TestKnownVehicleSettings(OpenpilotTestCase):
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("toyota"))} keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("toyota"))}
assert "ToyotaEnforceStockLongitudinal" in keys assert "ToyotaEnforceStockLongitudinal" in keys
assert "ToyotaStopAndGoHack" in keys assert "ToyotaStopAndGoHack" in keys
assert "ToyotaTSS2Smooth" in keys
def test_tesla_has_coop_steering(self, schema): def test_tesla_has_coop_steering(self, schema):
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("tesla"))} keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("tesla"))}