mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-10-27 13:13:46 +08:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be3bd6bafd | |||
| 663e9c5e42 | |||
| de56d68fa8 | |||
| b914f168b4 | |||
| 07bb42c2bb | |||
| dd674fbc0d | |||
| 53176484bd | |||
| b2d025f543 | |||
| 35ee46abc3 | |||
| 567cffdb3e | |||
| 7cf2f4cf2e | |||
| 1b4902a8ba | |||
| 895bfa8ea5 | |||
| 45051dab54 | |||
| 612c79760a | |||
| af93efd608 |
@@ -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
Submodule opendbc_repo updated: 3022d3367c...e3f1487de4
@@ -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.
|
||||||
|
|||||||
@@ -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"))}
|
||||||
|
|||||||
Reference in New Issue
Block a user