mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-11-04 23:23:45 +08:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d60707fce8 | |||
| 554451138f | |||
| 88b3ed1a20 | |||
| 3c73898aed | |||
| a60b6f5804 | |||
| 3b235b2bad | |||
| 8bdb2f54d9 | |||
| 9c1bdde65b | |||
| 8b3a19eb04 | |||
| 367ff603d4 | |||
| a86979d2b2 | |||
| 914aaefac1 | |||
| d05649c0a4 | |||
| 3521140920 | |||
| 56628fc770 | |||
| 60a9cc8657 |
@@ -4,6 +4,7 @@
|
||||
[submodule "opendbc"]
|
||||
path = opendbc_repo
|
||||
url = https://github.com/sunnypilot/opendbc.git
|
||||
branch = toyotabsmhack
|
||||
[submodule "msgq"]
|
||||
path = msgq_repo
|
||||
url = https://github.com/sunnypilot/msgq.git
|
||||
|
||||
+1
-1
Submodule opendbc_repo updated: aa9bff6f54...a0f52f61af
@@ -149,6 +149,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"Version", {PERSISTENT, STRING}},
|
||||
|
||||
// --- sunnypilot params --- //
|
||||
{"AccelBoostSP", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
||||
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
@@ -199,6 +200,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"TripSummary", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
{"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
// MADS params
|
||||
@@ -244,6 +246,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"TeslaCoopSteering", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"TeslaMadsScreenButton", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"ToyotaEnhancedBsm", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaTSS2Smooth", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
@@ -7,7 +7,6 @@ from openpilot.common.constants import CV
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.selfdrive.controls.lib.accel_boost import AccelBoost
|
||||
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, LongitudinalPlanSource
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC
|
||||
@@ -38,7 +37,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
LongitudinalPlannerSP.__init__(self, self.CP, CP_SP, self.mpc)
|
||||
self.fcw = False
|
||||
self.dt = dt
|
||||
self.accel_boost = AccelBoost()
|
||||
|
||||
self.v_desired_filter = FirstOrderFilter(init_v, 2.0, self.dt)
|
||||
self.a_cruise = init_a
|
||||
|
||||
@@ -10,6 +10,7 @@ from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
|
||||
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
||||
from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow
|
||||
from openpilot.selfdrive.ui.body.layouts.onroad import BodyLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.trip_summary import TripSummary
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout
|
||||
@@ -31,6 +32,7 @@ class MainLayout(Widget):
|
||||
self._sidebar = Sidebar()
|
||||
self._current_mode = MainState.HOME
|
||||
self._prev_onroad = False
|
||||
self._trip_summary = TripSummary()
|
||||
|
||||
# Initialize layouts
|
||||
self._home_layout = HomeLayout()
|
||||
|
||||
@@ -10,6 +10,7 @@ from openpilot.selfdrive.ui.body.layouts.onroad import BodyLayout
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.trip_summary import TripSummary
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout
|
||||
@@ -57,6 +58,7 @@ class MiciMainLayout(Scroller):
|
||||
self._setup_callbacks()
|
||||
|
||||
gui_app.add_nav_stack_tick(self._handle_transitions)
|
||||
self._trip_summary = TripSummary()
|
||||
gui_app.push_widget(self)
|
||||
|
||||
# Start onboarding if terms or training not completed, make sure to push after self
|
||||
|
||||
@@ -22,6 +22,7 @@ from openpilot.common.transformations.orientation import rot_from_euler
|
||||
from enum import IntEnum
|
||||
|
||||
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.ui_state import OnroadTimerStatus
|
||||
|
||||
@@ -181,6 +182,7 @@ class AugmentedRoadView(CameraView):
|
||||
self._alert_renderer = AlertRenderer()
|
||||
self._driver_state_renderer = DriverStateRenderer()
|
||||
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,
|
||||
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||
alignment=TextAlignment.CENTER,
|
||||
@@ -274,6 +276,11 @@ class AugmentedRoadView(CameraView):
|
||||
# Use self._content_rect for positioning within camera bounds
|
||||
if show_navigation:
|
||||
self._navigation.render(rl.Rectangle(self._content_rect.x + self._content_rect.width, self.rect.y, PANEL_WIDTH, self.rect.height))
|
||||
elif self._accel_boost_indicator is not None:
|
||||
# The boost bar owns the side panel while the feature is on; the ball only shows otherwise
|
||||
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)
|
||||
|
||||
|
||||
@@ -12,10 +12,16 @@ from openpilot.system.ui.widgets import DialogResult
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
from opendbc.car.toyota.values import ToyotaFlags
|
||||
from opendbc.sunnypilot.car.toyota.values import ToyotaFlagsSP
|
||||
|
||||
|
||||
ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.")
|
||||
SNG_HACK_UNAVAILABLE = tr_noop("sunnypilot Longitudinal Control must be available and enabled for your vehicle to use this feature.")
|
||||
BSM_UNAVAILABLE = tr_noop("Enhanced Blind Spot Monitoring requires a TSS2 Toyota/Lexus without SecOC, " +
|
||||
"or a smartDSU with sunnypilot longitudinal control.")
|
||||
BSM_STOCK_LONG_UNAVAILABLE = tr_noop("Enhanced Blind Spot Monitoring is not available while Enforce Factory Longitudinal Control is enabled.")
|
||||
BSM_REQUIRED = tr_noop("This vehicle does not report blind spot status over the standard CAN signals. " +
|
||||
"Enhanced Blind Spot Monitoring is required for blind spot detection.")
|
||||
PLATFORM_UNAVAILABLE = tr_noop("This feature is currently not available on this platform.")
|
||||
|
||||
DESCRIPTIONS = {
|
||||
@@ -26,6 +32,10 @@ DESCRIPTIONS = {
|
||||
'sunnypilot will allow some Toyota/Lexus cars to auto resume during stop and go traffic. ' +
|
||||
'This feature is only applicable to certain models that are able to use longitudinal control. This is an alpha feature. Use at your own risk.'
|
||||
),
|
||||
'enhanced_bsm': tr_noop(
|
||||
'sunnypilot will poll the blind spot monitor diagnostics directly from the radar. Use this on vehicles whose blind spot monitors ' +
|
||||
'do not report blind spot status over the standard CAN signals. This is an alpha feature. Use at your own risk.'
|
||||
),
|
||||
'tss2_smooth': tr_noop(
|
||||
'Enable smoother longitudinal control for TSS2 vehicles. ' +
|
||||
'Uses a custom speed-dependent integral gain curve for smoother traffic driving.'
|
||||
@@ -53,6 +63,14 @@ class ToyotaSettings(BrandSettings):
|
||||
enabled=lambda: not ui_state.engaged,
|
||||
)
|
||||
|
||||
self.enhanced_bsm = toggle_item_sp(
|
||||
lambda: tr("Enhanced Blind Spot Monitoring (Alpha)"),
|
||||
description=lambda: tr(DESCRIPTIONS["enhanced_bsm"]),
|
||||
initial_state=ui_state.params.get_bool("ToyotaEnhancedBsm"),
|
||||
callback=self._on_enable_enhanced_bsm,
|
||||
enabled=lambda: not ui_state.engaged,
|
||||
)
|
||||
|
||||
self.tss2_smooth = toggle_item_sp(
|
||||
lambda: tr("TSS2 Smooth Longitudinal (Alpha)"),
|
||||
description=lambda: tr(DESCRIPTIONS["tss2_smooth"]),
|
||||
@@ -64,6 +82,7 @@ class ToyotaSettings(BrandSettings):
|
||||
self.items = [
|
||||
self.enforce_stock_longitudinal,
|
||||
self.stop_and_go_hack,
|
||||
self.enhanced_bsm,
|
||||
self.tss2_smooth,
|
||||
]
|
||||
|
||||
@@ -130,6 +149,25 @@ class ToyotaSettings(BrandSettings):
|
||||
ui_state.params.put_bool("ToyotaStopAndGoHack", False)
|
||||
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def _on_enable_enhanced_bsm(self, state: bool):
|
||||
if state:
|
||||
def confirm_callback(result: int):
|
||||
if result == DialogResult.CONFIRM:
|
||||
ui_state.params.put_bool("ToyotaEnhancedBsm", True)
|
||||
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||
else:
|
||||
self.enhanced_bsm.action_item.set_state(False)
|
||||
|
||||
content = (f"<h1>{self.enhanced_bsm.title}</h1><br>" +
|
||||
f"<p>{self.enhanced_bsm.description}</p>")
|
||||
|
||||
dlg = ConfirmDialog(content, tr("Enable"), rich=True, callback=confirm_callback)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
else:
|
||||
ui_state.params.put_bool("ToyotaEnhancedBsm", False)
|
||||
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def update_settings(self):
|
||||
if ui_state.CP is not None:
|
||||
longitudinal = ui_state.CP.openpilotLongitudinalControl
|
||||
@@ -150,6 +188,31 @@ class ToyotaSettings(BrandSettings):
|
||||
if show_desc:
|
||||
self.stop_and_go_hack.show_description(True)
|
||||
|
||||
# Enhanced BSM needs the radar diagnostic address, which is only in the TX allowlist for
|
||||
# TSS2 cars without SecOC, or for TSS-P cars whose smartDSU lets sunnypilot control
|
||||
# longitudinal. Enforcing factory longitudinal control rules both of those out.
|
||||
sdsu_bsm_supported = (longitudinal and ui_state.CP_SP is not None and
|
||||
bool(ui_state.CP_SP.flags & ToyotaFlagsSP.SMART_DSU))
|
||||
bsm_supported = (not bool(ui_state.CP.flags & ToyotaFlags.SECOC) and
|
||||
(bool(ui_state.CP.flags & ToyotaFlags.TSS2) or sdsu_bsm_supported))
|
||||
bsm_available = bsm_supported and not enforce_stock
|
||||
self.enhanced_bsm.action_item.set_enabled(bsm_available and not ui_state.engaged)
|
||||
|
||||
bsm_desc = tr(DESCRIPTIONS["enhanced_bsm"])
|
||||
if not bsm_supported:
|
||||
self.enhanced_bsm.action_item.set_state(False)
|
||||
bsm_desc = "<b>" + tr(BSM_UNAVAILABLE) + "</b>\n\n" + bsm_desc
|
||||
elif enforce_stock:
|
||||
self.enhanced_bsm.action_item.set_state(False)
|
||||
bsm_desc = "<b>" + tr(BSM_STOCK_LONG_UNAVAILABLE) + "</b>\n\n" + bsm_desc
|
||||
elif ui_state.CP_SP is not None and ui_state.CP_SP.flags & ToyotaFlagsSP.SP_NEED_DEBUG_BSM:
|
||||
bsm_desc = "<b>" + tr(BSM_REQUIRED) + "</b>\n\n" + bsm_desc
|
||||
|
||||
if self.enhanced_bsm.description != bsm_desc:
|
||||
self.enhanced_bsm.set_description(bsm_desc)
|
||||
if not bsm_available:
|
||||
self.enhanced_bsm.show_description(True)
|
||||
|
||||
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)
|
||||
@@ -173,6 +236,12 @@ class ToyotaSettings(BrandSettings):
|
||||
self.stop_and_go_hack.set_description(new_desc)
|
||||
self.stop_and_go_hack.show_description(True)
|
||||
|
||||
self.enhanced_bsm.action_item.set_enabled(False)
|
||||
bsm_desc = "<b>" + tr(ONROAD_ONLY_DESCRIPTION) + "</b>\n\n" + tr(DESCRIPTIONS["enhanced_bsm"])
|
||||
if self.enhanced_bsm.description != bsm_desc:
|
||||
self.enhanced_bsm.set_description(bsm_desc)
|
||||
self.enhanced_bsm.show_description(True)
|
||||
|
||||
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:
|
||||
|
||||
@@ -72,6 +72,11 @@ class VisualsLayout(Widget):
|
||||
"It is the driver's responsibility to observe their environment and make decisions accordingly."),
|
||||
None,
|
||||
),
|
||||
"TripSummary": (
|
||||
lambda: tr("Trip Summary"),
|
||||
tr("Show a summary of distance and engagement when you shift into park after a drive."),
|
||||
None,
|
||||
),
|
||||
"TrueVEgoUI": (
|
||||
lambda: tr("Speedometer: Always Display True Speed"),
|
||||
tr("For applicable vehicles, always display the true vehicle current speed from wheel speed sensors."),
|
||||
@@ -131,7 +136,7 @@ class VisualsLayout(Widget):
|
||||
super()._update_state()
|
||||
|
||||
for param in self._toggle_defs:
|
||||
self._toggles[param].action_item.set_state(self._params.get_bool(param))
|
||||
self._toggles[param].action_item.set_state(bool(self._params.get(param, return_default=True)))
|
||||
|
||||
self._dev_ui_info.action_item.set_selected_button(ui_state.params.get("DevUIInfo", return_default=True))
|
||||
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import time
|
||||
import pyray as rl
|
||||
|
||||
from opendbc.car.structs import car
|
||||
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.ui.onroad.augmented_road_view import BORDER_COLORS
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
AUTO_DISMISS = 20.
|
||||
FADE_IN, FADE_OUT = 0.35, 0.3
|
||||
LAT = (UIStatus.ENGAGED, UIStatus.LAT_ONLY)
|
||||
LONG = (UIStatus.ENGAGED, UIStatus.LONG_ONLY)
|
||||
GEAR = car.CarState.GearShifter
|
||||
FORWARD = (GEAR.drive, GEAR.sport, GEAR.low, GEAR.brake, GEAR.eco, GEAR.manumatic)
|
||||
|
||||
|
||||
class TripTracker:
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.segments: list[list] = []
|
||||
self.seconds = 0.
|
||||
self.takeovers = 0
|
||||
self._t = None
|
||||
self._status = None
|
||||
|
||||
def update(self, driving: bool):
|
||||
now = time.monotonic()
|
||||
if not driving:
|
||||
self._t = None
|
||||
self._status = None
|
||||
return
|
||||
dt = 0. if self._t is None else min(now - self._t, 0.5)
|
||||
self._t = now
|
||||
self.seconds += dt
|
||||
if self._status not in (None, UIStatus.DISENGAGED) and ui_state.status == UIStatus.DISENGAGED:
|
||||
self.takeovers += 1
|
||||
self._status = ui_state.status
|
||||
d = max(ui_state.sm["carState"].vEgo, 0.) * dt
|
||||
if d <= 0.:
|
||||
return
|
||||
if self.segments and self.segments[-1][0] == ui_state.status:
|
||||
self.segments[-1][1] += d
|
||||
else:
|
||||
self.segments.append([ui_state.status, d])
|
||||
|
||||
def total(self, statuses=None) -> float:
|
||||
return sum(m for s, m in self.segments if statuses is None or s in statuses)
|
||||
|
||||
|
||||
class TripSummary:
|
||||
def __init__(self):
|
||||
self._trip = TripTracker()
|
||||
self._dialog: TripSummaryDialog | None = None
|
||||
self._gear = None
|
||||
self._drove = False
|
||||
gui_app.add_nav_stack_tick(self._tick)
|
||||
|
||||
def _tick(self):
|
||||
gear = ui_state.sm["carState"].gearShifter if ui_state.started else None
|
||||
self._trip.update(gear not in (None, GEAR.park))
|
||||
self._drove |= gear in FORWARD
|
||||
if gear == self._gear:
|
||||
return
|
||||
self._gear = gear
|
||||
|
||||
if gear not in (None, GEAR.park):
|
||||
self._pop()
|
||||
elif gear == GEAR.park and self._drove and ui_state.trip_summary and not ui_state.is_body and self._trip.total() > 0.:
|
||||
self._dialog = TripSummaryDialog(self._trip, gui_app.get_active_widget(), self._pop)
|
||||
gui_app.push_widget(self._dialog)
|
||||
if gear is None:
|
||||
self._trip.reset()
|
||||
if gear in (None, GEAR.park):
|
||||
self._drove = False
|
||||
|
||||
def _pop(self):
|
||||
if self._dialog is not None and gui_app.get_active_widget() is self._dialog:
|
||||
gui_app.pop_widget()
|
||||
self._dialog = None
|
||||
|
||||
|
||||
def _ease(t: float) -> float:
|
||||
t = min(max(t, 0.), 1.)
|
||||
return 1 - (1 - t) ** 3
|
||||
|
||||
|
||||
def _fade(c, a: float) -> rl.Color:
|
||||
r, g, b, al = c if isinstance(c, tuple) else (c.r, c.g, c.b, c.a)
|
||||
return rl.Color(r, g, b, int(al * a))
|
||||
|
||||
|
||||
class TripSummaryDialog(Widget):
|
||||
def __init__(self, trip: TripTracker, below: Widget | None, on_closed):
|
||||
super().__init__()
|
||||
self._below = below
|
||||
self._on_closed = on_closed
|
||||
self._segments = segments = trip.segments
|
||||
total = sum(m for _, m in segments)
|
||||
|
||||
def pct(statuses):
|
||||
return 100 * sum(m for s, m in segments if s in statuses) / total
|
||||
|
||||
dist = total * (CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH) / 3600
|
||||
dist_fmt = "{:.1f}" if dist < 9.95 else "{:.0f}"
|
||||
self._stats = [
|
||||
(dist, dist_fmt, "km" if ui_state.is_metric else "mi", "driven"),
|
||||
(pct(LAT + LONG), "{:.0f}", "%", "engaged"),
|
||||
(pct(LAT), "{:.0f}", "%", "lateral"),
|
||||
(pct(LONG), "{:.0f}", "%", "longitudinal"),
|
||||
]
|
||||
mins = round(trip.seconds / 60)
|
||||
self._details = [
|
||||
(f"{mins // 60}h {mins % 60}" if mins >= 60 else f"{mins}", "min"),
|
||||
(f"{trip.takeovers}", "takeover" if trip.takeovers == 1 else "takeovers"),
|
||||
]
|
||||
self._bold = gui_app.font(FontWeight.BOLD)
|
||||
self._semi = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
self._normal = gui_app.font(FontWeight.NORMAL)
|
||||
self._big = gui_app.big_ui()
|
||||
self._shown_t = rl.get_time()
|
||||
self._closing_t: float | None = None
|
||||
self.set_click_callback(self.close)
|
||||
|
||||
def close(self):
|
||||
if self._closing_t is None:
|
||||
self._closing_t = rl.get_time()
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
t = rl.get_time() - self._shown_t
|
||||
if self._closing_t is None and t > AUTO_DISMISS:
|
||||
self.close()
|
||||
out = 0. if self._closing_t is None else (rl.get_time() - self._closing_t) / FADE_OUT
|
||||
if out >= 1.:
|
||||
self._on_closed()
|
||||
return
|
||||
bg = 1 - _ease(out)
|
||||
a = _ease(t / FADE_IN) * bg
|
||||
|
||||
if self._big and self._below is not None and bg < 1.:
|
||||
self._below.render(rect)
|
||||
rl.draw_rectangle_rec(rect, _fade(rl.BLACK, bg))
|
||||
|
||||
k = rect.height / 1080 * (1.0 if self._big else 1.22)
|
||||
pad = 120 * k
|
||||
x, w = rect.x + pad, rect.width - 2 * pad
|
||||
y = rect.y + (rect.height - 850 * k) / 2
|
||||
|
||||
rl.draw_text_ex(self._bold, "trip summary", rl.Vector2(x, y), int(150 * k), 0, _fade(rl.WHITE, a))
|
||||
y += 260 * k
|
||||
|
||||
col_w = w / len(self._stats)
|
||||
for i, (target, fmt, unit, label) in enumerate(self._stats):
|
||||
cx = x + col_w * (i + 0.5)
|
||||
v = measure_text_cached(self._bold, fmt.format(target), int(170 * k))
|
||||
u = measure_text_cached(self._semi, unit, int(80 * k))
|
||||
vx = cx - (v.x + 10 * k + u.x) / 2
|
||||
rl.draw_text_ex(self._bold, fmt.format(target), rl.Vector2(vx, y), int(170 * k), 0, _fade(rl.WHITE, a))
|
||||
rl.draw_text_ex(self._semi, unit, rl.Vector2(vx + v.x + 10 * k, y + 75 * k), int(80 * k), 0, _fade(rl.WHITE, a))
|
||||
lb = measure_text_cached(self._normal, label, int(70 * k))
|
||||
rl.draw_text_ex(self._normal, label, rl.Vector2(cx - lb.x / 2, y + 180 * k), int(70 * k), 0, _fade(rl.Color(200, 200, 200, 255), a))
|
||||
y += 330 * k
|
||||
|
||||
bar = rl.Rectangle(x, y, w, 90 * k)
|
||||
total = sum(m for _, m in self._segments)
|
||||
sx = bar.x
|
||||
for status, m in self._segments:
|
||||
sw = bar.width * m / total
|
||||
rl.draw_rectangle_rec(rl.Rectangle(sx, bar.y, min(sw + 1, bar.x + bar.width - sx), bar.height), _fade(BORDER_COLORS[status], a))
|
||||
sx += sw
|
||||
rl.draw_rectangle_rounded_lines_ex(bar, 1.0, 20, 30 * k, _fade(rl.BLACK, a))
|
||||
|
||||
y += (90 + 70) * k
|
||||
|
||||
size = int(90 * k)
|
||||
parts = []
|
||||
for i, (value, unit) in enumerate(self._details):
|
||||
if i:
|
||||
parts.append((" ", self._normal, None))
|
||||
parts += [(value, self._bold, rl.WHITE), (" " + unit, self._normal, rl.Color(170, 170, 170, 255))]
|
||||
widths = [measure_text_cached(f, t, size).x for t, f, _ in parts]
|
||||
lx = x + (w - sum(widths)) / 2
|
||||
for (text, font, color), tw in zip(parts, widths, strict=True):
|
||||
if color is None:
|
||||
rl.draw_circle(int(lx + tw / 2), int(y + size * 0.6), 12 * k, _fade(rl.Color(110, 110, 110, 255), a))
|
||||
else:
|
||||
rl.draw_text_ex(font, text, rl.Vector2(lx, y), size, 0, _fade(color, a))
|
||||
lx += tw
|
||||
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_boost import ACCEL_BOOST_MAX
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
BAR_WIDTH = 26
|
||||
BAR_RADIUS = 0.5
|
||||
TRACK_TOP_RATIO = 0.08
|
||||
TRACK_BOTTOM_RATIO = 0.92
|
||||
# Fades to white only near the profile cap, like the stock torque bar fading toward orange at high torque
|
||||
CAP_BLEND_START = 0.70
|
||||
MAX_BOOST_BLEND = 0.55
|
||||
|
||||
COLOR_TRACK = rl.Color(255, 255, 255, 32)
|
||||
COLOR_TRACK_EDGE = rl.Color(255, 255, 255, 70)
|
||||
COLOR_HIGHLIGHT = rl.Color(255, 255, 255, 45)
|
||||
|
||||
AGGRESSIVE_COLOR = rl.Color(255, 80, 60, 255)
|
||||
STANDARD_COLOR = rl.Color(0, 230, 120, 255)
|
||||
RELAXED_COLOR = rl.Color(70, 150, 255, 255)
|
||||
|
||||
# The personality enum is ordered aggressive, standard, relaxed, matching ACCEL_BOOST_MAX
|
||||
PROFILE_STYLES = {
|
||||
0: (AGGRESSIVE_COLOR, ACCEL_BOOST_MAX[0]),
|
||||
1: (STANDARD_COLOR, ACCEL_BOOST_MAX[1]),
|
||||
2: (RELAXED_COLOR, ACCEL_BOOST_MAX[2]),
|
||||
}
|
||||
|
||||
FADE_THRESHOLD = 0.01
|
||||
|
||||
|
||||
def get_boost_style(personality) -> tuple[rl.Color, float]:
|
||||
"""Color and full-scale boost for the active personality."""
|
||||
raw = getattr(personality, "raw", personality)
|
||||
return PROFILE_STYLES.get(int(raw), PROFILE_STYLES[1])
|
||||
|
||||
|
||||
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 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)
|
||||
|
||||
|
||||
class AccelBoostIndicator(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
fps = max(gui_app.target_fps, 1)
|
||||
self._boost_filter = FirstOrderFilter(0.0, 0.15, 1 / fps)
|
||||
self._alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / fps)
|
||||
# Smoothly rescale the bar when the follow distance profile changes mid-drive
|
||||
self._max_boost_filter = FirstOrderFilter(0.0, 0.25, 1 / fps)
|
||||
self._profile_color, self._max_boost = PROFILE_STYLES[1]
|
||||
|
||||
self.panel_in_use = False
|
||||
|
||||
def _update_state(self):
|
||||
sm = ui_state.sm
|
||||
visible = (ui_state.has_longitudinal_control and sm.alive['selfdriveState'] and
|
||||
sm.alive['longitudinalPlan'] and sm['selfdriveState'].enabled)
|
||||
self._alpha_filter.update(1.0 if visible else 0.0)
|
||||
self._boost_filter.update(float(sm['longitudinalPlan'].accelBoost) if visible else 0.0)
|
||||
|
||||
if sm.alive['selfdriveState']:
|
||||
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 while it is drawn; the confidence ball returns when it fades out
|
||||
self.panel_in_use = self._alpha_filter.x > FADE_THRESHOLD
|
||||
|
||||
def _render(self, _):
|
||||
alpha = self._alpha_filter.x
|
||||
if alpha < FADE_THRESHOLD or self._max_boost <= 0.0:
|
||||
return
|
||||
|
||||
panel_x = self.rect.x + self.rect.width - SIDE_PANEL_WIDTH
|
||||
cx = panel_x + SIDE_PANEL_WIDTH / 2
|
||||
bar_x = cx - BAR_WIDTH / 2
|
||||
track_top = self.rect.y + self.rect.height * TRACK_TOP_RATIO
|
||||
track_bottom = self.rect.y + self.rect.height * TRACK_BOTTOM_RATIO
|
||||
track_h = max(1.0, track_bottom - track_top)
|
||||
track_rect = rl.Rectangle(bar_x, track_top, BAR_WIDTH, track_h)
|
||||
|
||||
# soft well behind the track so the bar reads on the black side panel
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(bar_x - 3, track_top - 3, BAR_WIDTH + 6, track_h + 6), BAR_RADIUS, 12,
|
||||
rl.Color(0, 0, 0, int(80 * alpha)))
|
||||
|
||||
# track, light enough to read on the black side panel
|
||||
rl.draw_rectangle_rounded(track_rect, BAR_RADIUS, 12,
|
||||
rl.Color(COLOR_TRACK.r, COLOR_TRACK.g, COLOR_TRACK.b, int(COLOR_TRACK.a * alpha)))
|
||||
rl.draw_rectangle_rounded_lines_ex(track_rect, BAR_RADIUS, 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
|
||||
if fill_h <= 0.0:
|
||||
return
|
||||
|
||||
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)
|
||||
tip_y = track_bottom - fill_h
|
||||
|
||||
# soft glow around the tip, like the confidence ball's gradient
|
||||
tip_center_y = tip_y + min(fill_h, BAR_WIDTH) / 2
|
||||
glow = rl.Color(fill_color.r, fill_color.g, fill_color.b, int(85 * alpha))
|
||||
rl.draw_circle_gradient(rl.Vector2(cx, tip_center_y), int(BAR_WIDTH * 0.85), 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, tip_y, BAR_WIDTH, fill_h), BAR_RADIUS, 12, fill)
|
||||
|
||||
# subtle inner highlight gives the fill a rounded, glassy edge
|
||||
if fill_h > 12:
|
||||
highlight_rect = rl.Rectangle(bar_x + 4, tip_y + 5, 3, fill_h - 10)
|
||||
rl.draw_rectangle_rounded(highlight_rect, 0.5, 8,
|
||||
rl.Color(COLOR_HIGHLIGHT.r, COLOR_HIGHLIGHT.g, COLOR_HIGHLIGHT.b,
|
||||
int(COLOR_HIGHLIGHT.a * alpha)))
|
||||
|
||||
# bright cap at the tip
|
||||
rl.draw_circle(int(cx), int(tip_center_y), 2,
|
||||
rl.Color(255, 255, 255, int(150 * alpha)))
|
||||
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.cereal import log
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
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 import ACCEL_BOOST_MAX
|
||||
|
||||
|
||||
class TestAccelBoostIndicator(OpenpilotTestCase):
|
||||
def test_profile_styles_match_controller_caps(self):
|
||||
for personality, color, max_boost in (
|
||||
(log.LongitudinalPersonality.aggressive, AGGRESSIVE_COLOR, ACCEL_BOOST_MAX[0]),
|
||||
(log.LongitudinalPersonality.standard, STANDARD_COLOR, ACCEL_BOOST_MAX[1]),
|
||||
(log.LongitudinalPersonality.relaxed, RELAXED_COLOR, ACCEL_BOOST_MAX[2])):
|
||||
style_color, style_max = get_boost_style(personality)
|
||||
assert style_color == color
|
||||
assert style_max == max_boost
|
||||
|
||||
def test_wire_personality_picks_the_right_profile(self):
|
||||
msg = log.SelfdriveState.new_message()
|
||||
msg.personality = log.LongitudinalPersonality.aggressive
|
||||
color, max_boost = get_boost_style(msg.personality)
|
||||
assert color == AGGRESSIVE_COLOR
|
||||
assert max_boost == ACCEL_BOOST_MAX[0]
|
||||
|
||||
def test_bar_fills_at_the_active_profile_cap(self):
|
||||
for cap in ACCEL_BOOST_MAX:
|
||||
assert get_boost_fraction(0.0, cap) == 0.0
|
||||
assert get_boost_fraction(cap * 0.5, cap) == 0.5
|
||||
assert get_boost_fraction(cap, cap) == 1.0
|
||||
|
||||
# A boost beyond the current cap (e.g. after switching to a shorter profile) still fills the bar
|
||||
assert get_boost_fraction(max(ACCEL_BOOST_MAX), min(ACCEL_BOOST_MAX)) == 1.0
|
||||
assert 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)
|
||||
assert relaxed.b > relaxed.r
|
||||
assert relaxed.b >= relaxed.g
|
||||
assert aggressive.r > aggressive.b
|
||||
# lighter in RGB means the dimmest channel came up towards white
|
||||
assert relaxed.r > RELAXED_COLOR.r
|
||||
assert aggressive.g > AGGRESSIVE_COLOR.g
|
||||
@@ -48,6 +48,7 @@ class UIStateSP:
|
||||
self.model_runner_tinygrad: bool = False
|
||||
self.active_runner_tinygrad: bool = False
|
||||
self.blindspot: bool = False
|
||||
self.trip_summary: bool = True
|
||||
self.chevron_metrics = None
|
||||
self.custom_interactive_timeout: int = 0
|
||||
self.developer_ui = None
|
||||
@@ -185,6 +186,7 @@ class UIStateSP:
|
||||
self.standstill_timer = self.params.get_bool("StandstillTimer")
|
||||
self.sunnylink_enabled = self.params.get_bool("SunnylinkEnabled")
|
||||
self.torque_bar = self.params.get_bool("TorqueBar")
|
||||
self.trip_summary = bool(self.params.get("TripSummary", return_default=True))
|
||||
self.enforce_torque_control = self.params.get_bool("EnforceTorqueControl")
|
||||
self.custom_torque_params = self.params.get_bool("CustomTorqueParams")
|
||||
self.torque_override_enabled = self.params.get_bool("TorqueParamsOverrideEnabled")
|
||||
|
||||
@@ -126,6 +126,7 @@ def initialize_params(params) -> list[dict[str, Any]]:
|
||||
keys.extend([
|
||||
"ToyotaEnforceStockLongitudinal",
|
||||
"ToyotaStopAndGoHack",
|
||||
"ToyotaEnhancedBsm",
|
||||
"ToyotaTSS2Smooth",
|
||||
])
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
from openpilot.cereal import log
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
|
||||
|
||||
ACCEL_BOOST_MIN_SPEED = 1.0
|
||||
ACCEL_BOOST_MAX = (0.4, 0.25, 0.1) # aggressive, standard, relaxed, m/s^2
|
||||
ACCEL_BOOST_BUILD, ACCEL_BOOST_RELEASE = 0.2, 1.0 # m/s^3
|
||||
BRAKE_PROB = (0.1, 0.3) # the model's brake press in 2 s: it wants to slow
|
||||
GAS_PROB = (0.6, 0.8) # predicts a human gas press at this hypothesis at about 23%
|
||||
CURVE_LAT_ACCEL = (1.5, 2.5)
|
||||
|
||||
|
||||
def get_boost_max(personality=log.LongitudinalPersonality.standard):
|
||||
if personality == log.LongitudinalPersonality.relaxed:
|
||||
return ACCEL_BOOST_MAX[2]
|
||||
elif personality == log.LongitudinalPersonality.standard:
|
||||
return ACCEL_BOOST_MAX[1]
|
||||
elif personality == log.LongitudinalPersonality.aggressive:
|
||||
return ACCEL_BOOST_MAX[0]
|
||||
else:
|
||||
raise NotImplementedError("Longitudinal personality not supported")
|
||||
|
||||
|
||||
class AccelBoostSP:
|
||||
def __init__(self):
|
||||
self.params = Params()
|
||||
self.frame = -1
|
||||
self.enabled = False
|
||||
self.boost = 0.0
|
||||
self.total_boost = 0.0
|
||||
|
||||
def update_params(self):
|
||||
self.frame += 1
|
||||
if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) != 0:
|
||||
return False
|
||||
enabled = self.params.get_bool("AccelBoostSP")
|
||||
if enabled == self.enabled:
|
||||
return False
|
||||
self.enabled = enabled
|
||||
self.boost = self.total_boost = 0.0
|
||||
return True
|
||||
|
||||
def update(self, sm, output_a_target_e2e, output_a_target_mpc, a_cruise):
|
||||
model = sm['modelV2']
|
||||
brake_probs = model.meta.disengagePredictions.brakePressProbs
|
||||
gas_probs = model.meta.disengagePredictions.gasPressProbs
|
||||
if not sm['selfdriveState'].enabled or len(brake_probs) < 2 or len(gas_probs) < 2:
|
||||
self.boost = self.total_boost = 0.0
|
||||
return output_a_target_e2e
|
||||
|
||||
want = max(float(np.interp(brake_probs[1], BRAKE_PROB, [1.0, 0.0])), float(np.interp(gas_probs[1], GAS_PROB, [0.0, 1.0])))
|
||||
want = 0.0 if model.action.shouldStop else want
|
||||
want *= float(np.interp(abs(model.action.desiredCurvature) * sm['carState'].vEgo ** 2, CURVE_LAT_ACCEL, [1.0, 0.0]))
|
||||
target = want * get_boost_max(sm['selfdriveState'].personality)
|
||||
self.boost = float(np.clip(target, self.boost - ACCEL_BOOST_RELEASE * DT_MDL, self.boost + ACCEL_BOOST_BUILD * DT_MDL))
|
||||
speed_scale = np.interp(sm['carState'].vEgo, [ACCEL_BOOST_MIN_SPEED, 2 * ACCEL_BOOST_MIN_SPEED], [0.0, 1.0])
|
||||
self.total_boost = self.boost * speed_scale * np.interp(output_a_target_e2e, [-0.7, -0.2, 5.0], [0.0, 1.0, 1.0], right=0.0)
|
||||
return output_a_target_e2e + self.total_boost
|
||||
@@ -9,6 +9,8 @@ from openpilot.cereal import messaging, custom
|
||||
from opendbc.car import structs
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX
|
||||
from openpilot.selfdrive.controls.lib.accel_boost import AccelBoost
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_boost import AccelBoostSP
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.e2e_alerts_helper import E2EAlertsHelper
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.smart_cruise_control import SmartCruiseControl
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist
|
||||
@@ -29,10 +31,16 @@ class LongitudinalPlannerSP:
|
||||
self.generation = int(model_bundle.generation) if (model_bundle := get_active_bundle()) else None
|
||||
self.source = LongitudinalPlanSource.cruise
|
||||
self.e2e_alerts_helper = E2EAlertsHelper()
|
||||
self.accel_boost_stock = AccelBoost()
|
||||
self.accel_boost_sp = AccelBoostSP()
|
||||
|
||||
self.output_v_target = 0.
|
||||
self.output_a_target = 0.
|
||||
|
||||
@property
|
||||
def accel_boost(self):
|
||||
return self.accel_boost_sp if self.accel_boost_sp.enabled else self.accel_boost_stock
|
||||
|
||||
def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]:
|
||||
CS = sm['carState']
|
||||
v_cruise_cluster_kph = min(CS.vCruiseCluster, V_CRUISE_MAX)
|
||||
@@ -64,6 +72,9 @@ class LongitudinalPlannerSP:
|
||||
return self.output_v_target, self.output_a_target
|
||||
|
||||
def update(self, sm: messaging.SubMaster) -> None:
|
||||
if self.accel_boost_sp.update_params():
|
||||
self.accel_boost_stock = AccelBoost()
|
||||
|
||||
self.events_sp.clear()
|
||||
self.e2e_alerts_helper.update(sm, self.events_sp)
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from openpilot.common.parameterized import parameterized
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
|
||||
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_boost import AccelBoostSP, ACCEL_BOOST_MAX
|
||||
|
||||
|
||||
def make_sm(brake_prob=0.05, gas_prob=0.3, lat_accel=0.0, v_ego=30.0, personality=0, enabled=True, should_stop=False):
|
||||
meta = SimpleNamespace(disengagePredictions=SimpleNamespace(brakePressProbs=[0.0, brake_prob, 0.0, 0.0, 0.0, 0.0],
|
||||
gasPressProbs=[0.0, gas_prob, 0.0, 0.0, 0.0, 0.0]))
|
||||
action = SimpleNamespace(shouldStop=should_stop, desiredCurvature=lat_accel / v_ego ** 2)
|
||||
return {'modelV2': SimpleNamespace(meta=meta, action=action), 'carState': SimpleNamespace(vEgo=v_ego),
|
||||
'selfdriveState': SimpleNamespace(enabled=enabled, personality=personality)}
|
||||
|
||||
|
||||
def settle(sm, frames=100):
|
||||
boost = AccelBoostSP()
|
||||
for _ in range(frames):
|
||||
boost.update(sm, 0.0, 9.0, 9.0)
|
||||
return boost.total_boost
|
||||
|
||||
|
||||
class TestAccelBoostSP(OpenpilotTestCase):
|
||||
@parameterized.expand([(0,), (1,), (2,)])
|
||||
def test_personality_max(self, personality):
|
||||
assert abs(settle(make_sm(personality=personality)) - ACCEL_BOOST_MAX[personality]) < 1e-6
|
||||
|
||||
def test_brake_gate_closes(self):
|
||||
assert settle(make_sm(brake_prob=0.5)) == 0.0
|
||||
|
||||
def test_gas_reopens(self):
|
||||
assert abs(settle(make_sm(brake_prob=0.5, gas_prob=0.9)) - ACCEL_BOOST_MAX[0]) < 1e-6
|
||||
|
||||
def test_should_stop(self):
|
||||
assert settle(make_sm(gas_prob=0.9, should_stop=True)) == 0.0
|
||||
|
||||
@parameterized.expand([(1.5, 1.0), (2.0, 0.5), (2.5, 0.0), (-2.0, 0.5)])
|
||||
def test_curve_fade(self, lat_accel, scale):
|
||||
assert abs(settle(make_sm(lat_accel=lat_accel)) - scale * ACCEL_BOOST_MAX[0]) < 1e-6
|
||||
|
||||
def test_low_speed(self):
|
||||
assert settle(make_sm(v_ego=0.5)) == 0.0
|
||||
|
||||
def test_disabled_resets(self):
|
||||
boost = AccelBoostSP()
|
||||
for _ in range(100):
|
||||
boost.update(make_sm(), 0.0, 9.0, 9.0)
|
||||
assert boost.update(make_sm(enabled=False), 0.0, 9.0, 9.0) == 0.0
|
||||
assert boost.boost == 0.0 and boost.total_boost == 0.0
|
||||
|
||||
def test_build_and_release_rates(self):
|
||||
boost = AccelBoostSP()
|
||||
boost.update(make_sm(), 0.0, 9.0, 9.0)
|
||||
assert abs(boost.total_boost - 0.01) < 1e-6
|
||||
for _ in range(100):
|
||||
boost.update(make_sm(), 0.0, 9.0, 9.0)
|
||||
boost.update(make_sm(brake_prob=0.5), 0.0, 9.0, 9.0)
|
||||
assert abs(boost.total_boost - (ACCEL_BOOST_MAX[0] - 0.05)) < 1e-6
|
||||
|
||||
def test_param_switch(self):
|
||||
params = Params()
|
||||
boost = AccelBoostSP()
|
||||
params.put_bool("AccelBoostSP", True, block=True)
|
||||
assert boost.update_params() and boost.enabled
|
||||
for _ in range(100):
|
||||
boost.update(make_sm(), 0.0, 9.0, 9.0)
|
||||
params.put_bool("AccelBoostSP", False, block=True)
|
||||
for _ in range(int(PARAMS_UPDATE_PERIOD / DT_MDL) - 1):
|
||||
assert not boost.update_params()
|
||||
assert boost.update_params() and not boost.enabled
|
||||
assert boost.boost == 0.0 and boost.total_boost == 0.0
|
||||
@@ -13,6 +13,7 @@ from opendbc.car.subaru.values import CAR as SUBARU_CAR, SubaruFlags
|
||||
from opendbc.car.toyota.values import CAR as TOYOTA_CAR, ToyotaFlags
|
||||
from opendbc.sunnypilot.car.interfaces import has_bsm
|
||||
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
|
||||
from opendbc.sunnypilot.car.toyota.values import ToyotaFlagsSP
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.hardware import HARDWARE
|
||||
@@ -44,6 +45,7 @@ CAPABILITY_FIELDS = (
|
||||
"device_type",
|
||||
"subaru_has_sng",
|
||||
"hyundai_alpha_long_available",
|
||||
"toyota_enhanced_bsm_available",
|
||||
"toyota_tss2",
|
||||
"custom_model_active",
|
||||
)
|
||||
@@ -68,6 +70,7 @@ CAPABILITY_LABELS: dict[str, str] = {
|
||||
"device_type": "Device type",
|
||||
"subaru_has_sng": "Subaru Stop-and-Go available",
|
||||
"hyundai_alpha_long_available": "Hyundai Alpha Longitudinal available",
|
||||
"toyota_enhanced_bsm_available": "Toyota Enhanced BSM available",
|
||||
"toyota_tss2": "Toyota TSS2 platform",
|
||||
"custom_model_active": "Non-Default model selected",
|
||||
}
|
||||
@@ -89,6 +92,7 @@ CAR_CAPABILITY_FIELDS = (
|
||||
"subaru_has_sng",
|
||||
"hyundai_alpha_long_available",
|
||||
"toyota_tss2",
|
||||
"toyota_enhanced_bsm_available",
|
||||
)
|
||||
|
||||
# Explicit defaults for non-boolean capability fields
|
||||
@@ -104,7 +108,7 @@ def _bundle_field(bundle: dict | None, key: str) -> str:
|
||||
return bundle.get(key, "") if isinstance(bundle, dict) else ""
|
||||
|
||||
|
||||
def _resolve_brand_capabilities(caps: dict, bundle_platform: str, CP) -> None:
|
||||
def _resolve_brand_capabilities(caps: dict, bundle_platform: str, CP, CP_SP=None) -> None:
|
||||
"""Set brand-specific capabilities from bundle platform or CarParams fallback.
|
||||
|
||||
Bundle (manual car selection) is a pre-fingerprint approximation.
|
||||
@@ -138,11 +142,18 @@ def _resolve_brand_capabilities(caps: dict, bundle_platform: str, CP) -> None:
|
||||
elif brand == "toyota":
|
||||
if bundle_platform:
|
||||
try:
|
||||
caps["toyota_tss2"] = bool(TOYOTA_CAR[bundle_platform].config.flags & ToyotaFlags.TSS2)
|
||||
flags = TOYOTA_CAR[bundle_platform].config.flags
|
||||
caps["toyota_tss2"] = bool(flags & ToyotaFlags.TSS2)
|
||||
caps["toyota_enhanced_bsm_available"] = bool(flags & ToyotaFlags.TSS2) and not bool(flags & ToyotaFlags.SECOC)
|
||||
except KeyError:
|
||||
cloudlog.exception(f"capabilities: unknown toyota platform {bundle_platform!r}")
|
||||
elif CP is not None:
|
||||
caps["toyota_tss2"] = bool(CP.flags & ToyotaFlags.TSS2)
|
||||
# same gate as _initialize_toyota: TSS2 without SecOC, or TSS-P with a smartDSU and openpilot
|
||||
# longitudinal control (the bundle path can't see 0x2FF, so it stays TSS2-only)
|
||||
sdsu_bsm = CP_SP is not None and bool(CP_SP.flags & ToyotaFlagsSP.SMART_DSU) and bool(CP.openpilotLongitudinalControl)
|
||||
caps["toyota_enhanced_bsm_available"] = (not bool(CP.flags & ToyotaFlags.SECOC) and
|
||||
(bool(CP.flags & ToyotaFlags.TSS2) or sdsu_bsm))
|
||||
|
||||
|
||||
def generate_capabilities(params: Params | None = None) -> dict:
|
||||
@@ -217,7 +228,7 @@ def generate_capabilities(params: Params | None = None) -> dict:
|
||||
if CP is not None:
|
||||
caps["has_bsm"] = has_bsm(CP, CP_SP)
|
||||
|
||||
_resolve_brand_capabilities(caps, bundle_platform, CP)
|
||||
_resolve_brand_capabilities(caps, bundle_platform, CP, CP_SP)
|
||||
|
||||
return caps
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ The tables below describe the **compiled** `settings_ui.json` schema — what th
|
||||
|
||||
**Enablement rules**: Grayed out (disabled) when rules fail. Frontend shows a contextual badge explaining why.
|
||||
|
||||
**Capability fields** (referenced in rules): `has_longitudinal_control`, `has_icbm`, `icbm_available`, `torque_allowed`, `brand`, `pcm_cruise`, `alpha_long_available`, `steer_control_type`, `has_bsm`, `is_release`, `is_sp_release`, `is_development`, `tesla_has_vehicle_bus`, `has_stop_and_go`, `stock_longitudinal`
|
||||
**Capability fields** (referenced in rules): `has_longitudinal_control`, `has_icbm`, `icbm_available`, `torque_allowed`, `brand`, `pcm_cruise`, `alpha_long_available`, `steer_control_type`, `has_bsm`, `is_release`, `is_sp_release`, `is_development`, `tesla_has_vehicle_bus`, `has_stop_and_go`, `stock_longitudinal`, `toyota_enhanced_bsm_available`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -620,6 +620,19 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "AccelBoostSP",
|
||||
"widget": "toggle",
|
||||
"title": "Sunnypilot Accel Boost",
|
||||
"description": "Adds acceleration, sized by driving personality, when the driving model predicts no reason to slow, so it reaches your set speed sooner. Fades out in tight curves.",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "has_longitudinal_control",
|
||||
"equals": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "IntelligentCruiseButtonManagement",
|
||||
"widget": "toggle",
|
||||
@@ -1349,7 +1362,17 @@
|
||||
"key": "TorqueBar",
|
||||
"widget": "toggle",
|
||||
"title": "Steering Arc",
|
||||
"description": "Display steering arc on the driving screen when lateral control is enabled."
|
||||
"description": "Display steering arc on the driving screen when lateral control is enabled.",
|
||||
"visibility": [
|
||||
{
|
||||
"type": "not",
|
||||
"condition": {
|
||||
"type": "capability",
|
||||
"field": "device_type",
|
||||
"equals": "mici"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ShowTurnSignals",
|
||||
@@ -1389,6 +1412,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "TripSummary",
|
||||
"widget": "toggle",
|
||||
"title": "Trip Summary",
|
||||
"description": "Show a summary of distance and engagement when you shift into park after a drive."
|
||||
},
|
||||
{
|
||||
"key": "RocketFuel",
|
||||
"widget": "toggle",
|
||||
@@ -2338,6 +2367,28 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ToyotaEnhancedBsm",
|
||||
"widget": "toggle",
|
||||
"needs_onroad_cycle": true,
|
||||
"title": "Enhanced Blind Spot Monitoring (Alpha)",
|
||||
"description": "sunnypilot will poll the blind spot monitor diagnostics directly from the radar. Use this on vehicles whose blind spot monitors do not report blind spot status over the standard CAN signals. This is an alpha feature. Use at your own risk.",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "not_engaged"
|
||||
},
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "toyota_enhanced_bsm_available",
|
||||
"equals": true
|
||||
},
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "stock_longitudinal",
|
||||
"equals": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ToyotaTSS2Smooth",
|
||||
"widget": "toggle",
|
||||
|
||||
@@ -30,6 +30,13 @@ sections:
|
||||
label: Relaxed
|
||||
enablement:
|
||||
- $ref: '#/macros/longitudinal'
|
||||
- key: AccelBoostSP
|
||||
widget: toggle
|
||||
title: Sunnypilot Accel Boost
|
||||
description: Adds acceleration, sized by driving personality, when the driving model predicts no reason to slow, so it
|
||||
reaches your set speed sooner. Fades out in tight curves.
|
||||
enablement:
|
||||
- $ref: '#/macros/longitudinal'
|
||||
- key: IntelligentCruiseButtonManagement
|
||||
widget: toggle
|
||||
title: Intelligent Cruise Button Management (ICBM) (Alpha)
|
||||
|
||||
@@ -101,6 +101,21 @@ sections:
|
||||
- type: param
|
||||
key: ToyotaEnforceStockLongitudinal
|
||||
equals: false
|
||||
- key: ToyotaEnhancedBsm
|
||||
widget: toggle
|
||||
needs_onroad_cycle: true
|
||||
title: Enhanced Blind Spot Monitoring (Alpha)
|
||||
description: sunnypilot will poll the blind spot monitor diagnostics directly from the radar. Use this on vehicles whose
|
||||
blind spot monitors do not report blind spot status over the standard CAN signals. This is an alpha feature. Use at
|
||||
your own risk.
|
||||
enablement:
|
||||
- $ref: '#/macros/not_engaged'
|
||||
- type: capability
|
||||
field: toyota_enhanced_bsm_available
|
||||
equals: true
|
||||
- type: capability
|
||||
field: stock_longitudinal
|
||||
equals: false
|
||||
- key: ToyotaTSS2Smooth
|
||||
widget: toggle
|
||||
needs_onroad_cycle: true
|
||||
|
||||
@@ -20,6 +20,8 @@ sections:
|
||||
widget: toggle
|
||||
title: Steering Arc
|
||||
description: Display steering arc on the driving screen when lateral control is enabled.
|
||||
visibility:
|
||||
- $ref: '#/macros/hide_on_mici'
|
||||
- key: ShowTurnSignals
|
||||
widget: toggle
|
||||
title: Display Turn Signals
|
||||
@@ -37,6 +39,10 @@ sections:
|
||||
description: Show a timer on the HUD when the car is at a standstill.
|
||||
visibility:
|
||||
- $ref: '#/macros/hide_on_mici'
|
||||
- key: TripSummary
|
||||
widget: toggle
|
||||
title: Trip Summary
|
||||
description: Show a summary of distance and engagement when you shift into park after a drive.
|
||||
- key: RocketFuel
|
||||
widget: toggle
|
||||
title: Real-time Acceleration Bar
|
||||
|
||||
@@ -70,12 +70,18 @@ class TestOpaquePerBrandFlags(OpenpilotTestCase):
|
||||
def test_hyundai_alpha_long_available_field_present(self):
|
||||
assert "hyundai_alpha_long_available" in CAPABILITY_FIELDS
|
||||
|
||||
def test_toyota_enhanced_bsm_available_field_present(self):
|
||||
assert "toyota_enhanced_bsm_available" in CAPABILITY_FIELDS
|
||||
|
||||
def test_subaru_has_sng_default_false(self, caps):
|
||||
assert caps["subaru_has_sng"] is False
|
||||
|
||||
def test_hyundai_alpha_long_available_default_false(self, caps):
|
||||
assert caps["hyundai_alpha_long_available"] is False
|
||||
|
||||
def test_toyota_enhanced_bsm_available_default_false(self, caps):
|
||||
assert caps["toyota_enhanced_bsm_available"] is False
|
||||
|
||||
def test_toyota_tss2_field_present(self):
|
||||
assert "toyota_tss2" in CAPABILITY_FIELDS
|
||||
|
||||
|
||||
@@ -207,6 +207,7 @@ class TestNotEngagedReplacement(OpenpilotTestCase):
|
||||
"AlphaLongitudinalEnabled",
|
||||
"ToyotaEnforceStockLongitudinal",
|
||||
"ToyotaStopAndGoHack",
|
||||
"ToyotaEnhancedBsm",
|
||||
"ToyotaTSS2Smooth",
|
||||
], names=["key"])
|
||||
def test_offroad_only_replaced_with_not_engaged(self, schema, key):
|
||||
|
||||
@@ -286,6 +286,7 @@ class TestKnownVehicleSettings(OpenpilotTestCase):
|
||||
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("toyota"))}
|
||||
assert "ToyotaEnforceStockLongitudinal" in keys
|
||||
assert "ToyotaStopAndGoHack" in keys
|
||||
assert "ToyotaEnhancedBsm" in keys
|
||||
assert "ToyotaTSS2Smooth" in keys
|
||||
|
||||
def test_tesla_has_coop_steering(self, schema):
|
||||
|
||||
Reference in New Issue
Block a user