Compare commits

...

12 Commits

Author SHA1 Message Date
Kumar fa4d9c1491 taper 2026-11-04 18:37:44 -07:00
rav4kumar 026decd535 smooth the final stop 2026-11-04 12:49:19 -07:00
rav4kumar d27e5e9b3a trip-summary 2026-11-04 12:19:10 -07:00
rav4kumar ac75030e70 boooooost-ui 2026-11-04 12:18:22 -07:00
Kumar 73c8b30fa9 Merge branch 'master' into toyota-bsm 2026-11-04 12:14:55 -07:00
rav4kumar 8bdb2f54d9 Merge branch 'master' into toyota-bsm 2026-11-03 10:37:16 -07:00
rav4kumar 9c1bdde65b toyota: enable the enhanced BSM toggle for TSS-P cars with a smartDSU 2026-11-03 10:31:07 -07:00
Kumar 914aaefac1 Merge branch 'master' into toyota-bsm 2026-10-26 11:35:14 -07:00
rav4kumar d05649c0a4 toyota: bump opendbc and fix BSM wiring 2026-10-26 11:32:51 -07:00
rav4kumar 3521140920 ref 2026-10-25 11:56:26 -07:00
rav4kumar 56628fc770 ref 2026-10-25 11:55:52 -07:00
rav4kumar 60a9cc8657 toyota: add Enhanced Blind Spot Monitoring setting 2026-10-25 11:55:32 -07:00
26 changed files with 692 additions and 9 deletions
+1
View File
@@ -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
+2
View File
@@ -200,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
@@ -245,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"}},
+1 -2
View File
@@ -18,7 +18,6 @@ from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD
from openpilot.selfdrive.controls.lib.latcontrol_curvature import LatControlCurvature
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
from openpilot.selfdrive.controls.lib.longcontrol import LongControl
from openpilot.selfdrive.modeld.modeld import LAT_SMOOTH_SECONDS
from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose
@@ -56,7 +55,7 @@ class Controls(ControlsExt):
self.pose_calibrator = PoseCalibrator()
self.calibrated_pose: Pose | None = None
self.LoC = LongControl(self.CP, self.CP_SP)
self.LoC = ControlsExt.initialize_longitudinal_control(self, self.CP, self.CP_SP)
self.VM = VehicleModel(self.CP)
self.LaC: LatControl
if self.CP.steerControlType == car.CarParams.SteerControlType.angle:
+2
View File
@@ -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.augmented_road_view import AugmentedRoadViewSP
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.hud_renderer import HudRendererSP as HudRenderer
from openpilot.selfdrive.ui.sunnypilot.ui_state import OnroadTimerStatus
@@ -156,9 +157,10 @@ class BookmarkIcon(Widget):
rl.draw_texture_ex(icon, rl.Vector2(icon_x, icon_y), 0.0, 1.0, rl.Color(255, 255, 255, round(255 * alpha)))
class AugmentedRoadView(CameraView):
class AugmentedRoadView(CameraView, AugmentedRoadViewSP):
def __init__(self, bookmark_callback=None, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_NARROW_ROAD):
super().__init__("camerad", stream_type)
AugmentedRoadViewSP.__init__(self)
self._bookmark_callback = bookmark_callback
self._set_placeholder_color(rl.BLACK)
@@ -275,7 +277,7 @@ class AugmentedRoadView(CameraView):
if show_navigation:
self._navigation.render(rl.Rectangle(self._content_rect.x + self._content_rect.width, self.rect.y, PANEL_WIDTH, self.rect.height))
else:
self._confidence_ball.render(self.rect)
AugmentedRoadViewSP.render_side_panel(self, self.rect)
self._bookmark_icon.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."),
@@ -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,95 @@
"""
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 math
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.sunnypilot.selfdrive.controls.lib.accel_boost import get_boost_max
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets import Widget
BAR_WIDTH = 26
TRACK_TOP_RATIO = 0.08
TRACK_BOTTOM_RATIO = 0.92
CAP_BLEND_START = 0.45
MAX_BOOST_BLEND = 0.55
FADE_THRESHOLD = 0.01
COLOR_TRACK = rl.Color(255, 255, 255, 28)
COLOR_FILL = rl.Color(0, 230, 120, 255)
def get_boost_fraction(boost: float, max_boost: float) -> float:
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:
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_color(fraction: float) -> rl.Color:
blend = max(0.0, (fraction - CAP_BLEND_START) / (1.0 - CAP_BLEND_START))
return lighten(COLOR_FILL, MAX_BOOST_BLEND * blend)
class AccelBoostIndicator(Widget):
def __init__(self, panel_width: float = SIDE_PANEL_WIDTH, bar_width: float = BAR_WIDTH):
super().__init__()
self._panel_width = panel_width
self._bar_width = bar_width
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)
self._max_boost_filter = FirstOrderFilter(get_boost_max(), 0.25, 1 / fps)
@property
def holding_panel(self) -> bool:
return self._alpha_filter.x > FADE_THRESHOLD
def _update_state(self):
sm = ui_state.sm
visible = (ui_state.accel_boost and ui_state.has_longitudinal_control and sm.alive['selfdriveState'] and
sm.alive['longitudinalPlan'] and sm['selfdriveState'].enabled)
plan = sm['longitudinalPlan']
applied = visible and plan.longitudinalPlanSource == log.LongitudinalPlan.LongitudinalPlanSource.e2e
self._boost_filter.update(float(plan.accelBoost) if applied else 0.0)
self._alpha_filter.update(1.0 if visible else 0.0)
if sm.alive['selfdriveState']:
self._max_boost_filter.update(get_boost_max(sm['selfdriveState'].personality))
def _render(self, _):
alpha = self._alpha_filter.x
if alpha < FADE_THRESHOLD:
return
panel_x = max(self.rect.x, self.rect.x + self.rect.width - self._panel_width)
bar_x = panel_x + (self._panel_width - self._bar_width) / 2
track_top = self.rect.y + self.rect.height * TRACK_TOP_RATIO
track = rl.Rectangle(bar_x, track_top, self._bar_width,
max(1.0, self.rect.height * (TRACK_BOTTOM_RATIO - TRACK_TOP_RATIO)))
rl.draw_rectangle_rounded(track, 0.5, 12,
rl.Color(COLOR_TRACK.r, COLOR_TRACK.g, COLOR_TRACK.b, int(COLOR_TRACK.a * alpha)))
fraction = get_boost_fraction(self._boost_filter.x, self._max_boost_filter.x)
fill_h = fraction * track.height
if fill_h < 1.0:
return
fill_color = get_boost_color(fraction)
tip = track.y + track.height - fill_h
rl.begin_scissor_mode(int(track.x), int(tip), int(math.ceil(track.width)) + 1, int(math.ceil(fill_h)))
rl.draw_rectangle_rounded(track, 0.5, 12, rl.Color(fill_color.r, fill_color.g, fill_color.b, int(235 * alpha)))
rl.end_scissor_mode()
@@ -0,0 +1,19 @@
"""
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.selfdrive.ui.sunnypilot.mici.onroad.accel_boost_indicator import AccelBoostIndicator
class AugmentedRoadViewSP:
def __init__(self):
self._accel_boost_indicator = AccelBoostIndicator()
def render_side_panel(self, rect: rl.Rectangle) -> None:
self._accel_boost_indicator.render(rect)
if not self._accel_boost_indicator.holding_panel:
self._confidence_ball.render(rect)
@@ -0,0 +1,36 @@
"""
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 (
CAP_BLEND_START, COLOR_FILL, get_boost_color, get_boost_fraction,
)
from openpilot.sunnypilot.selfdrive.controls.lib.accel_boost import ACCEL_BOOST_MAX, get_boost_max
class TestAccelBoostIndicator(OpenpilotTestCase):
def test_bar_fills_at_the_active_profile_cap(self):
for personality in (log.LongitudinalPersonality.aggressive, log.LongitudinalPersonality.standard,
log.LongitudinalPersonality.relaxed):
cap = get_boost_max(personality)
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
assert get_boost_fraction(max(ACCEL_BOOST_MAX), min(ACCEL_BOOST_MAX)) == 1.0
assert get_boost_fraction(0.0, 0.0) == 0.0
assert get_boost_fraction(-1.0, get_boost_max()) == 0.0
def test_fill_lightens_as_boost_builds(self):
base = get_boost_color(0.0)
assert (base.r, base.g, base.b) == (COLOR_FILL.r, COLOR_FILL.g, COLOR_FILL.b)
assert (get_boost_color(CAP_BLEND_START).r, get_boost_color(CAP_BLEND_START).b) == (COLOR_FILL.r, COLOR_FILL.b)
full = get_boost_color(1.0)
assert full.r > base.r
assert full.b > base.b
assert full.g > full.r and full.g > full.b
@@ -44,10 +44,12 @@ class UIStateSP:
self.screensaver = ScreenSaverSP(params=self.params)
self.screensaver_enabled: bool = False
self.accel_boost: bool = False
self.active_bundle = None
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
@@ -171,6 +173,7 @@ class UIStateSP:
# stock only counts the default big model's compiled pkl. a downloaded big bundle runs on the
# chestnut just the same, so ChestnutState has to see it as available too.
self.chestnut_compiled = self.chestnut_compiled or self.model_runner_tinygrad
self.accel_boost = self.params.get_bool("AccelBoostSP")
self.blindspot = self.params.get_bool("BlindSpot")
self.chevron_metrics = self.params.get("ChevronInfo")
self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True)
@@ -185,6 +188,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 = self.params.get_bool("TripSummary")
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",
])
@@ -17,6 +17,7 @@ from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
from openpilot.sunnypilot.selfdrive.controls.lib.blinker_pause_lateral import BlinkerPauseLateral
from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_v0 import LatControlTorque as LatControlTorqueV0
from openpilot.sunnypilot.selfdrive.controls.lib.longcontrol_sp import LongControlSP
class ControlsExt(ModelStateBase):
@@ -34,6 +35,9 @@ class ControlsExt(ModelStateBase):
self.sm_services_ext = ['radarState', 'selfdriveStateSP']
self.pm_services_ext = ['carControlSP']
def initialize_longitudinal_control(self, CP: structs.CarParams, CP_SP: custom.CarParamsSP) -> LongControlSP:
return LongControlSP(CP, CP_SP)
def initialize_lateral_control(self, lac, CI, dt):
enforce_torque_control = self.params.get_bool("EnforceTorqueControl")
torque_versions = self.params.get("TorqueControlTune")
@@ -0,0 +1,91 @@
"""
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 numpy as np
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.controls.lib.longcontrol import LongControl, LongCtrlState
STOP_JERK = 0.25
TAPER_MAX_DECEL = 1.0
STOP_LEAD_TIME = 0.4
STOP_STALL_TIME = 1.5
STOP_RELEASE_DEBOUNCE = 0.2
MAX_JERK = 2.0
def taper_decel(v_ego: float) -> float:
return float(np.sqrt(2.0 * STOP_JERK * max(v_ego, 0.0)))
class StopSmoother:
def __init__(self, stop_accel: float):
self.stop_accel = stop_accel
self.last_accel = 0.0
self.holding = False
self.stall_time = 0.0
def reset(self, accel: float = 0.0) -> None:
self.last_accel = accel
self.holding = False
self.stall_time = 0.0
def update(self, v_ego: float, a_ego: float, standstill: bool, accel_limits: tuple[float, float]) -> float:
self.holding = self.holding or standstill
if self.stop_accel >= 0.0:
target_accel = min(self.last_accel, 0.0)
elif self.holding:
target_accel = self.stop_accel
else:
self.stall_time += DT_CTRL
stall_decel = STOP_JERK * (self.stall_time - STOP_STALL_TIME)
v_lead = v_ego + min(a_ego, 0.0) * STOP_LEAD_TIME
target_accel = max(self.stop_accel, -max(taper_decel(v_lead), stall_decel))
max_change = MAX_JERK * DT_CTRL
accel = self.last_accel + np.clip(target_accel - self.last_accel, -max_change, max_change)
self.last_accel = float(np.clip(accel, *accel_limits))
return self.last_accel
class LongControlSP(LongControl):
def __init__(self, CP, CP_SP):
super().__init__(CP, CP_SP)
self.stop_smoother = StopSmoother(CP.stopAccel)
self.release_time = 0.0
self.was_tapering = False
def debounce_should_stop(self, CS, should_stop: bool) -> bool:
if self.stop_smoother.holding and CS.standstill and not should_stop:
self.release_time += DT_CTRL
return self.release_time < STOP_RELEASE_DEBOUNCE
self.release_time = 0.0
return should_stop
def update(self, active, CS, a_target, should_stop, accel_limits):
prev_output = self.last_output_accel
tapering = False
if -TAPER_MAX_DECEL < a_target < 0.0:
tapered = max(a_target, -taper_decel(CS.vEgo))
tapering = tapered != a_target
a_target = tapered
should_stop = self.debounce_should_stop(CS, should_stop)
output_accel = super().update(active, CS, a_target, should_stop, accel_limits)
if self.long_control_state == LongCtrlState.stopping:
output_accel = self.stop_smoother.update(CS.vEgo, CS.aEgo, CS.standstill, accel_limits)
else:
if self.long_control_state == LongCtrlState.pid and (tapering or self.was_tapering):
max_change = MAX_JERK * DT_CTRL
output_accel = float(np.clip(output_accel, prev_output - max_change, prev_output + max_change))
self.stop_smoother.reset(output_accel)
self.last_output_accel = output_accel
self.was_tapering = tapering
return float(output_accel)
@@ -0,0 +1,86 @@
"""
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 opendbc.car.structs import car
from openpilot.common.realtime import DT_CTRL
from openpilot.common.test import OpenpilotTestCase
from openpilot.cereal import custom
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
from openpilot.sunnypilot.selfdrive.controls.lib.longcontrol_sp import LongControlSP, STOP_ACCEL_RAMP, STOP_ACCEL_TAPER_GAIN
class TestLongControlSP(OpenpilotTestCase):
CP_SP = custom.CarParamsSP.new_message()
def make_control(self, stop_accel=-1.5):
CP = car.CarParams.new_message()
CP.stopAccel = stop_accel
CP.longitudinalTuning.kiBP = [0.0]
CP.longitudinalTuning.kiV = [0.0]
return LongControlSP(CP, self.CP_SP)
@staticmethod
def make_cs(v_ego, a_ego=0.0, brake_pressed=False, standstill=False):
return SimpleNamespace(vEgo=v_ego, aEgo=a_ego, brakePressed=brake_pressed, standstill=standstill,
cruiseState=SimpleNamespace(standstill=standstill))
def settle(self, loc, cs, frames=300, limits=(-2.0, 2.0)):
for _ in range(frames):
out = loc.update(True, cs, -0.5, should_stop=True, accel_limits=limits)
return out
def test_no_step_at_handover(self):
loc = self.make_control()
out = loc.update(True, self.make_cs(0.5, a_ego=-0.5), -0.5, should_stop=False, accel_limits=(-2.0, 2.0))
assert loc.long_control_state == LongCtrlState.pid
assert abs(out - -0.5) < 1e-6
out = loc.update(True, self.make_cs(0.3, a_ego=-0.5), -0.5, should_stop=True, accel_limits=(-2.0, 2.0))
assert loc.long_control_state == LongCtrlState.stopping
assert abs(out - (-0.5 - STOP_ACCEL_RAMP * DT_CTRL)) < 1e-6
def test_no_step_from_positive_accel(self):
loc = self.make_control()
loc.update(True, self.make_cs(0.4), 0.2, should_stop=False, accel_limits=(-2.0, 2.0))
out = loc.update(True, self.make_cs(0.25), 0.2, should_stop=True, accel_limits=(-2.0, 2.0))
assert loc.long_control_state == LongCtrlState.stopping
assert abs(out - (0.2 - STOP_ACCEL_RAMP * DT_CTRL)) < 1e-6
def test_decel_tapers_with_speed(self):
for v_ego in (1.0, 0.3, 0.1):
out = self.settle(self.make_control(), self.make_cs(v_ego))
assert abs(out - max(-1.5, -STOP_ACCEL_TAPER_GAIN * v_ego)) < 1e-6
def test_holds_at_standstill(self):
assert abs(self.settle(self.make_control(), self.make_cs(0.0)) - -1.5) < 1e-6
assert abs(self.settle(self.make_control(), self.make_cs(0.5, standstill=True)) - -1.5) < 1e-6
def test_car_holds_itself(self):
loc = self.make_control(stop_accel=0.0)
loc.update(True, self.make_cs(0.5), -0.4, should_stop=False, accel_limits=(-2.0, 2.0))
assert abs(self.settle(loc, self.make_cs(0.2)) - -0.4) < 1e-6
def test_stop_change_is_jerk_limited(self):
loc = self.make_control()
v_ego = 0.3
prev = loc.update(True, self.make_cs(v_ego), -0.5, should_stop=True, accel_limits=(-2.0, 2.0))
for _ in range(100):
out = loc.update(True, self.make_cs(v_ego), -0.5, should_stop=True, accel_limits=(-2.0, 2.0))
assert abs(out - prev) <= STOP_ACCEL_RAMP * DT_CTRL + 1e-9
prev = out
def test_repeatable(self):
runs = []
for _ in range(2):
loc = self.make_control()
run = []
for i in range(200):
v_ego = max(1.0 - i * 0.01, 0.0)
run.append(loc.update(True, self.make_cs(v_ego), -0.5, should_stop=v_ego < 0.3, accel_limits=(-2.0, 2.0)))
runs.append(run)
assert runs[0] == runs[1]
+14 -3
View File
@@ -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`
---
@@ -1412,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",
@@ -2361,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",
@@ -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
@@ -39,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):