Compare commits

..

3 Commits

Author SHA1 Message Date
Kumar a4e9059466 Merge branch 'master' into toyota-bsm 2026-10-21 11:29:22 -07:00
rav4kumar 628bda0bbc ref 2026-10-19 14:15:40 -07:00
rav4kumar 05493840f3 toyota: add Enhanced Blind Spot Monitoring setting 2026-10-19 14:04:55 -07:00
33 changed files with 151 additions and 1126 deletions
-53
View File
@@ -3,62 +3,9 @@ on:
pull_request:
paths:
- 'tinygrad_repo'
- 'openpilot/sunnypilot/modeld_v2/**'
workflow_dispatch:
env:
CI: 1
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: lfs.fetchexclude
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
jobs:
plain-artifact:
name: Plain artifact round trip and pickle converter
runs-on: ubuntu-24.04
env:
PYTHONPATH: ${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}
TINYGRAD_TESTS: 1
steps:
- uses: actions/checkout@v7
- run: ./tools/op.sh setup
- name: Build openpilot
run: scons -j$(nproc)
- name: Compile, load, cache and fuse a model with the pinned tinygrad
env:
DEV: CPU
run: tools/op.sh test -v openpilot/sunnypilot/modeld_v2/tests/test_plain_artifact.py
- name: Install LLVM 20 and ROCm comgr for the amd mock, as tinygrad_repo/.github/actions/setup-tinygrad does
run: |
. /etc/os-release
curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc > /dev/null
echo "deb http://apt.llvm.org/$VERSION_CODENAME/ llvm-toolchain-$VERSION_CODENAME-20 main" | sudo tee /etc/apt/sources.list.d/llvm.list
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.radeon.com/rocm/rocm.gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/rocm.gpg > /dev/null
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/rocm.gpg] https://repo.radeon.com/rocm/apt/7.1 $VERSION_CODENAME main" | sudo tee /etc/apt/sources.list.d/rocm.list
printf 'Package: *\nPin: release o=repo.radeon.com\nPin-Priority: 600\n' | sudo tee /etc/apt/preferences.d/rocm-pin-600
sudo apt-get -qq update && sudo apt-get -qq install -y --no-install-recommends libllvm20 clang-20 lld-20 comgr
printf '/opt/rocm/lib\n/opt/rocm/lib64\n' | sudo tee /etc/ld.so.conf.d/rocm.conf && sudo ldconfig
echo "/usr/lib/llvm-20/bin" >> "$GITHUB_PATH"
- name: Convert a pickled USB+AMD jit and replay it under tinygrad's amd mock
env:
DEV: MOCKUSB+AMD
CHECK_OOB: 0
GMMU: 0
run: tools/op.sh test -v openpilot/sunnypilot/modeld_v2/tests/test_convert_upstream_pkl.py
- name: Where tinygrad moved, for the helper or failure point named above
if: failure() && github.event_name == 'pull_request'
run: |
git fetch -q --depth=1 origin "${{ github.base_ref }}"
old=$(git rev-parse "origin/${{ github.base_ref }}:tinygrad_repo") new=$(git rev-parse HEAD:tinygrad_repo)
git -C tinygrad_repo fetch -q origin "$old" || true
echo "tinygrad_repo $old -> $new, in what helpers.py and plain_artifact_compiler.py read:"
git -C tinygrad_repo diff "$old" "$new" -- tinygrad/uop/ops.py tinygrad/engine/jit.py tinygrad/engine/realize.py tinygrad/device.py \
tinygrad/helpers.py tinygrad/dtype.py tinygrad/renderer/__init__.py
echo "and in what convert_upstream_pkl.py decodes:"
git -C tinygrad_repo diff --stat "$old" "$new" -- tinygrad/runtime/autogen/am/pm4_nv.py tinygrad/runtime/autogen/amd_gpu.py
git -C tinygrad_repo diff "$old" "$new" -- tinygrad/runtime/support/hcq2.py tinygrad/runtime/support/usb.py tinygrad/runtime/ops_amd.py
generate-matrix:
runs-on: ubuntu-latest
outputs:
+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
+1 -1
View File
@@ -170,7 +170,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}},
{"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}},
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
{"FollowDistanceAccelBoost", {PERSISTENT | BACKUP, BOOL, "0"}},
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
{"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}},
{"HasAcceptedTermsSP", {PERSISTENT, STRING, "0"}},
@@ -247,6 +246,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"TeslaMadsScreenButton", {PERSISTENT | BACKUP, INT, "0"}},
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaEnhancedBsm", {PERSISTENT | BACKUP, BOOL, "0"}},
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -1,101 +0,0 @@
from types import SimpleNamespace
from openpilot.cereal import log
from openpilot.common.realtime import DT_MDL
from openpilot.common.test import OpenpilotTestCase
from openpilot.selfdrive.controls.lib.accel_boost import (
AccelBoost, ACCEL_BOOST_MAX, ACCEL_BOOST_PER_OVERRIDE, ACCEL_BOOST_RATE
)
E2E_ACCEL = 0.5
PLANNER_ACCEL = 2.0
RATE_PER_STEP = ACCEL_BOOST_RATE * DT_MDL
class FakeSubMaster:
def __init__(self, enabled=True, gas_pressed=False, v_ego=15.0,
personality=log.LongitudinalPersonality.standard):
self._messages = {
'selfdriveState': SimpleNamespace(enabled=enabled, personality=personality),
'carState': SimpleNamespace(gasPressed=gas_pressed, vEgo=v_ego),
}
def __getitem__(self, key):
return self._messages[key]
def run_boost(boost, sm, steps=1):
out = E2E_ACCEL
for _ in range(steps):
out = boost.update(sm, E2E_ACCEL, PLANNER_ACCEL, PLANNER_ACCEL)
return out
class TestAccelBoost(OpenpilotTestCase):
def _boost_to_max(self):
boost = AccelBoost()
sm = FakeSubMaster()
steps = round(ACCEL_BOOST_PER_OVERRIDE / RATE_PER_STEP)
# Eligibility is latched on a frame without gas, then applied on the next override.
run_boost(boost, sm)
for _ in range(10):
sm['carState'].gasPressed = True
run_boost(boost, sm, steps)
sm['carState'].gasPressed = False
run_boost(boost, sm)
return boost
def test_no_boost_without_gas(self):
boost = AccelBoost()
sm = FakeSubMaster(gas_pressed=False)
out = run_boost(boost, sm)
self.assertEqual(boost.total_boost, 0.0)
self.assertAlmostEqual(out, E2E_ACCEL)
def test_boost_requires_e2e_to_be_limiting(self):
boost = AccelBoost()
sm = FakeSubMaster(gas_pressed=False)
# Eligibility is latched before the override, so keep the e2e plan at the planner limit here.
boost.update(sm, E2E_ACCEL, E2E_ACCEL, E2E_ACCEL)
sm['carState'].gasPressed = True
for _ in range(10):
boost.update(sm, E2E_ACCEL, E2E_ACCEL, E2E_ACCEL)
self.assertEqual(boost.total_boost, 0.0)
def test_per_override_cap(self):
boost = AccelBoost()
sm = FakeSubMaster(gas_pressed=False)
run_boost(boost, sm)
sm['carState'].gasPressed = True
run_boost(boost, sm, 1000)
self.assertAlmostEqual(boost.total_boost, ACCEL_BOOST_PER_OVERRIDE)
def test_max_boost(self):
boost = self._boost_to_max()
self.assertAlmostEqual(boost.total_boost, ACCEL_BOOST_MAX)
def test_boost_is_applied_to_e2e_accel(self):
boost = self._boost_to_max()
sm = FakeSubMaster(gas_pressed=True)
out = run_boost(boost, sm)
self.assertAlmostEqual(out, E2E_ACCEL + boost.total_boost)
def test_boost_does_not_depend_on_following_distance(self):
totals = []
for personality in (log.LongitudinalPersonality.relaxed,
log.LongitudinalPersonality.standard,
log.LongitudinalPersonality.aggressive):
boost = AccelBoost()
sm = FakeSubMaster(personality=personality)
steps = round(ACCEL_BOOST_PER_OVERRIDE / RATE_PER_STEP)
run_boost(boost, sm)
for _ in range(10):
sm['carState'].gasPressed = True
run_boost(boost, sm, steps)
sm['carState'].gasPressed = False
run_boost(boost, sm)
totals.append(boost.total_boost)
self.assertEqual(len(totals), 3)
for total in totals:
self.assertAlmostEqual(total, ACCEL_BOOST_MAX)
@@ -21,9 +21,6 @@ class TogglesLayoutMici(NavScroller):
"Use the steering wheel distance button on supported cars.")
self._initial_cruise_speed = InitialCruiseSpeedButton()
self._initial_cruise_speed.set_visible(lambda: ui_state.CP is not None and ui_state.has_longitudinal_control and not ui_state.CP.pcmCruise)
self._accel_boost_toggle = BigParamControl("extra acceleration boost", "FollowDistanceAccelBoost",
description="Adds extra acceleration on top of the stock gas-override boost.\n" +
"The extra boost follows Following Distance: strongest when Aggressive, weakest when Relaxed.")
is_metric_toggle = BigToggle("use metric units", initial_state=ui_state.params.get_bool("IsMetric"), toggle_callback=self._on_metric_toggle)
ldw_toggle = BigParamControl("lane departure warnings", "IsLdwEnabled",
description="Warn when you drift across a detected lane line.\n" +
@@ -43,7 +40,6 @@ class TogglesLayoutMici(NavScroller):
self._scroller.add_widgets([
self._personality_toggle,
self._accel_boost_toggle,
self._initial_cruise_speed,
is_metric_toggle,
ldw_toggle,
@@ -55,7 +51,6 @@ class TogglesLayoutMici(NavScroller):
# Toggle lists
self._refresh_toggles = (
("FollowDistanceAccelBoost", self._accel_boost_toggle),
("IsMetric", is_metric_toggle),
("IsLdwEnabled", ldw_toggle),
("AlwaysOnDM", always_on_dm_toggle),
@@ -97,7 +92,6 @@ class TogglesLayoutMici(NavScroller):
if ui_state.CP is not None:
self._personality_toggle.set_visible(ui_state.has_longitudinal_control)
self._accel_boost_toggle.set_visible(ui_state.has_longitudinal_control)
# Refresh toggles from params to mirror external changes
for key, item in self._refresh_toggles:
@@ -1,141 +0,0 @@
"""
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.cereal import log
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget
from openpilot.sunnypilot.selfdrive.controls.lib.accel_boost_ext import get_accel_boost_max
# The readout is wider than the side panel, so the value is right-aligned and bleeds over the camera view
BAR_WIDTH = 24
# Fades to white only near the profile cap, like the stock torque bar fading toward orange at high torque
CAP_BLEND_START = 0.75
MAX_BOOST_BLEND = 0.45
COLOR_TRACK = rl.Color(255, 255, 255, 45)
COLOR_TRACK_EDGE = rl.Color(255, 255, 255, 70)
COLOR_TEXT = rl.Color(255, 255, 255, 235)
COLOR_TEXT_SHADOW = rl.Color(0, 0, 0, 150)
# Temporary debug readout next to the bar, flip off once the bar alone reads right
SHOW_VALUE = True
VALUE_FONT_SIZE = 24
AGGRESSIVE_COLOR = rl.Color(255, 80, 60, 255)
STANDARD_COLOR = rl.Color(0, 230, 120, 255)
RELAXED_COLOR = rl.Color(70, 150, 255, 255)
FADE_THRESHOLD = 0.01
def lighten(color: rl.Color, amount: float) -> rl.Color:
"""Lerp towards white in RGB, so the hue stays put the way HSV blending would not."""
amount = min(max(amount, 0.0), 1.0)
return rl.Color(int(color.r + (255 - color.r) * amount),
int(color.g + (255 - color.g) * amount),
int(color.b + (255 - color.b) * amount), color.a)
def get_boost_fraction(boost: float, max_boost: float) -> float:
"""How much of the active profile's boost budget is in use, clamped to a full bar."""
if max_boost <= 0.0:
return 0.0
return min(max(boost / max_boost, 0.0), 1.0)
def get_boost_style(personality) -> tuple[rl.Color, float]:
if personality == log.LongitudinalPersonality.aggressive:
color = AGGRESSIVE_COLOR
elif personality == log.LongitudinalPersonality.relaxed:
color = RELAXED_COLOR
else:
color = STANDARD_COLOR
return color, get_accel_boost_max(personality)
class AccelBoostIndicator(Widget):
def __init__(self):
super().__init__()
self._boost_filter = FirstOrderFilter(0.0, 0.15, 1 / gui_app.target_fps)
self._alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
# Smoothly rescale the bar when the follow distance profile changes mid-drive
self._max_boost_filter = FirstOrderFilter(0.0, 0.25, 1 / gui_app.target_fps)
self._font = gui_app.font(FontWeight.BOLD)
self._profile_color = STANDARD_COLOR
self._max_boost = 0.0
self.panel_in_use = False
def _update_state(self):
sm = ui_state.sm
extra_enabled = ui_state.follow_distance_accel_boost
visible = (ui_state.has_longitudinal_control and sm.alive['selfdriveState'] and sm.alive['longitudinalPlan'] and
sm['selfdriveState'].enabled)
self._alpha_filter.update(1.0 if visible and extra_enabled else 0.0)
self._boost_filter.update(float(sm['longitudinalPlan'].accelBoost) if visible else 0.0)
if sm.alive['selfdriveState']:
self._profile_color, self._max_boost = get_boost_style(sm['selfdriveState'].personality)
self._max_boost_filter.update(self._max_boost)
self.panel_in_use = extra_enabled and ui_state.has_longitudinal_control
def _render(self, _):
alpha = self._alpha_filter.x
if alpha < FADE_THRESHOLD or self._max_boost <= 0.0:
return
cx = self.rect.x + self.rect.width - SIDE_PANEL_WIDTH / 2
bar_x = cx - BAR_WIDTH / 2
track_top = self.rect.y + self.rect.height * 0.08
track_bottom = self.rect.y + self.rect.height * 0.92
track_h = max(1.0, track_bottom - track_top)
# track, light enough to read on the black side panel
rl.draw_rectangle_rounded(rl.Rectangle(bar_x, track_top, BAR_WIDTH, track_h), 0.5, 12,
rl.Color(COLOR_TRACK.r, COLOR_TRACK.g, COLOR_TRACK.b, int(COLOR_TRACK.a * alpha)))
rl.draw_rectangle_rounded_lines_ex(rl.Rectangle(bar_x, track_top, BAR_WIDTH, track_h), 0.5, 12, 1,
rl.Color(COLOR_TRACK_EDGE.r, COLOR_TRACK_EDGE.g, COLOR_TRACK_EDGE.b,
int(COLOR_TRACK_EDGE.a * alpha)))
# Scale to the current profile's cap, so a maxed boost fills the bar and a profile change rescales it
boost = max(self._boost_filter.x, 0.0)
ratio = get_boost_fraction(boost, self._max_boost_filter.x)
fill_h = ratio * track_h
cap_blend = max(0.0, (ratio - CAP_BLEND_START) / (1.0 - CAP_BLEND_START))
fill_color = lighten(self._profile_color, MAX_BOOST_BLEND * cap_blend)
if fill_h > 0.0:
# soft glow around the tip, like the confidence ball's gradient
tip_y = track_bottom - fill_h
glow = rl.Color(fill_color.r, fill_color.g, fill_color.b, int(70 * alpha))
rl.draw_circle_gradient(rl.Vector2(cx, tip_y), int(BAR_WIDTH * 0.9), glow, rl.BLANK)
fill = rl.Color(fill_color.r, fill_color.g, fill_color.b, int(255 * alpha))
if fill_h < BAR_WIDTH:
rl.draw_circle(int(cx), int(track_bottom - fill_h / 2), max(1, int(BAR_WIDTH / 2)), fill)
else:
rl.draw_rectangle_rounded(rl.Rectangle(bar_x, track_bottom - fill_h, BAR_WIDTH, fill_h), 0.5, 12, fill)
if SHOW_VALUE:
value = f"+{boost:.2f}"
value_size = measure_text_cached(self._font, value, VALUE_FONT_SIZE)
value_pos = rl.Vector2(bar_x - 8 - value_size.x, (track_top + track_bottom) / 2 - value_size.y / 2)
self._draw_text(self._font, value, value_pos, VALUE_FONT_SIZE,
rl.Color(COLOR_TEXT.r, COLOR_TEXT.g, COLOR_TEXT.b, int(COLOR_TEXT.a * alpha)))
@staticmethod
def _draw_text(font, text: str, pos: rl.Vector2, font_size: int, color: rl.Color) -> None:
# Drop shadow keeps the readout legible against the camera feed
shadow = rl.Color(COLOR_TEXT_SHADOW.r, COLOR_TEXT_SHADOW.g, COLOR_TEXT_SHADOW.b, int(COLOR_TEXT_SHADOW.a * color.a / 255))
rl.draw_text_ex(font, text, rl.Vector2(pos.x + 1, pos.y + 1), font_size, 0, shadow)
rl.draw_text_ex(font, text, pos, font_size, 0, color)
@@ -5,7 +5,6 @@ from opendbc.car.structs import car
from openpilot.cereal.visionipc import VisionStreamType
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH
from openpilot.selfdrive.ui.mici.onroad.accel_boost_indicator import AccelBoostIndicator
from openpilot.selfdrive.ui.mici.onroad.alert_renderer import AlertRenderer
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer
@@ -182,7 +181,6 @@ class AugmentedRoadView(CameraView):
self._alert_renderer = AlertRenderer()
self._driver_state_renderer = DriverStateRenderer()
self._confidence_ball = ConfidenceBall()
self._accel_boost_indicator = AccelBoostIndicator()
self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY,
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
alignment=TextAlignment.CENTER,
@@ -277,10 +275,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:
# The boost bar owns the side panel while the extra boost is on; the ball only shows with the feature off
self._accel_boost_indicator.render(self.rect)
if not self._accel_boost_indicator.panel_in_use:
self._confidence_ball.render(self.rect)
self._confidence_ball.render(self.rect)
self._bookmark_icon.render(self.rect)
@@ -1,53 +0,0 @@
import unittest
from openpilot.cereal import log
from openpilot.selfdrive.controls.lib.accel_boost import ACCEL_BOOST_MAX
from openpilot.selfdrive.ui.mici.onroad.accel_boost_indicator import (get_boost_style, get_boost_fraction, lighten,
AGGRESSIVE_COLOR, RELAXED_COLOR, STANDARD_COLOR)
from openpilot.sunnypilot.selfdrive.controls.lib.accel_boost_ext import (SCALE_AGGRESSIVE, SCALE_RELAXED, SCALE_STANDARD,
get_accel_boost_max)
class TestAccelBoostIndicator(unittest.TestCase):
def test_bar_scales_with_following_distance(self):
for personality, color, scale in (
(log.LongitudinalPersonality.relaxed, RELAXED_COLOR, SCALE_RELAXED),
(log.LongitudinalPersonality.standard, STANDARD_COLOR, SCALE_STANDARD),
(log.LongitudinalPersonality.aggressive, AGGRESSIVE_COLOR, SCALE_AGGRESSIVE)):
bar_color, full_scale = get_boost_style(personality)
self.assertEqual(bar_color, color)
self.assertAlmostEqual(full_scale, ACCEL_BOOST_MAX * (1.0 + scale))
def test_bar_fills_at_the_active_profile_cap(self):
caps = [get_accel_boost_max(p) for p in (log.LongitudinalPersonality.relaxed,
log.LongitudinalPersonality.standard,
log.LongitudinalPersonality.aggressive)]
self.assertEqual(caps, sorted(caps))
for cap in caps:
self.assertAlmostEqual(get_boost_fraction(cap, cap), 1.0)
self.assertAlmostEqual(get_boost_fraction(0.0, cap), 0.0)
self.assertAlmostEqual(get_boost_fraction(cap * 0.5, cap), 0.5)
# A boost beyond the current cap (e.g. after switching to a shorter profile) still fills the bar
self.assertAlmostEqual(get_boost_fraction(caps[2], caps[0]), 1.0)
self.assertAlmostEqual(get_boost_fraction(0.0, 0.0), 0.0)
def test_cap_lighten_keeps_the_profile_hue(self):
# An HSV blend towards white sweeps relaxed blue through pink, so the fill lightens in RGB instead
relaxed = lighten(RELAXED_COLOR, 0.45)
aggressive = lighten(AGGRESSIVE_COLOR, 0.45)
self.assertGreater(relaxed.b, relaxed.r)
self.assertGreaterEqual(relaxed.b, relaxed.g)
self.assertGreater(aggressive.r, aggressive.b)
# lighter in RGB means the dimmest channel came up towards white
self.assertGreater(relaxed.r, RELAXED_COLOR.r)
self.assertGreater(aggressive.g, AGGRESSIVE_COLOR.g)
def test_wire_personality_picks_the_right_profile(self):
# Over the wire personality is a capnp DynamicEnum, which compares equal to the cereal enum but hashes differently
msg = log.SelfdriveState.new_message()
msg.personality = log.LongitudinalPersonality.aggressive
color, full_scale = get_boost_style(msg.personality)
self.assertEqual(color, AGGRESSIVE_COLOR)
self.assertAlmostEqual(full_scale, get_accel_boost_max(log.LongitudinalPersonality.aggressive))
@@ -30,11 +30,6 @@ ACC_NOLONG_DESCRIPTION = tr_noop("This feature can only be used with sunnypilot
ACC_PCMCRUISE_DISABLED_DESCRIPTION = tr_noop("This feature is not supported on this platform due to vehicle limitations.")
ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.")
ACCEL_BOOST_EXT_DESCRIPTION = tr_noop("Adds extra acceleration on top of the stock gas-override boost when you hold the gas pedal " +
"while sunnypilot longitudinal control is active. The extra boost follows the Following Distance setting: " +
"strongest when Aggressive, weakest when Relaxed. The stock boost remains active when this is off.")
ACCEL_BOOST_EXT_NOLONG_DESCRIPTION = tr_noop("This feature requires sunnypilot longitudinal control to be available.")
class CruiseLayout(Widget):
def __init__(self):
@@ -81,11 +76,6 @@ class CruiseLayout(Widget):
min_value=1, max_value=3, value_change_step=1,
inline=True)
self.accel_boost_toggle = toggle_item_sp(
title=tr("Extra Acceleration Boost by Following Distance"),
description="",
param="FollowDistanceAccelBoost")
self.sla_settings_button = simple_button_item_sp(
button_text=lambda: tr("Speed Limit"),
button_width=800,
@@ -96,7 +86,6 @@ class CruiseLayout(Widget):
self.icbm_toggle,
self.scc_v_toggle,
self.scc_m_toggle,
self.accel_boost_toggle,
self.custom_acc_toggle,
self.custom_acc_short_increment,
self.custom_acc_long_increment,
@@ -115,7 +104,6 @@ class CruiseLayout(Widget):
self._scroller.show_event()
self.icbm_toggle.show_description(True)
self.custom_acc_toggle.show_description(True)
self.accel_boost_toggle.show_description(True)
def _set_current_panel(self, panel: PanelType):
self._current_panel = panel
@@ -160,17 +148,10 @@ class CruiseLayout(Widget):
self.scc_v_toggle.action_item.set_enabled(False)
self.scc_m_toggle.action_item.set_enabled(False)
self.accel_boost_toggle.action_item.set_enabled(has_long)
boost_desc = tr(ACCEL_BOOST_EXT_DESCRIPTION) if has_long else tr(ACCEL_BOOST_EXT_NOLONG_DESCRIPTION)
if self.accel_boost_toggle.description != boost_desc:
self.accel_boost_toggle.set_description(boost_desc)
self.accel_boost_toggle.show_description(True)
else:
has_icbm = has_long = False
self.icbm_toggle.action_item.set_enabled(False)
self.icbm_toggle.set_description(tr(ONROAD_ONLY_DESCRIPTION))
self.accel_boost_toggle.action_item.set_enabled(False)
show_custom_acc_desc = False
@@ -243,9 +243,7 @@ class ModelsLayout(Widget):
if ds.verifying in statuses:
return {"name": name, "downloading": True, "progress": progress, "status_text": tr("verifying")}
if ds.downloading in statuses:
speed = max((p.speed for p in progresses), default=0.0)
status_text = f"{speed / 1024 ** 2:.1f} MB/s" if speed > 0 else "--.- MB/s"
return {"name": name, "downloading": True, "progress": progress, "status_text": status_text}
return {"name": name, "downloading": True, "progress": progress}
if statuses <= {ds.downloaded, ds.cached}:
return {"name": name, "text_color": ON_COLOR, "icon": "icons/checkmark.png"}
# circled_slash is authored grey; tinting it again only darkens it
@@ -4,6 +4,8 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from opendbc.car.toyota.values import ToyotaFlags
from opendbc.sunnypilot.car.toyota.values import ToyotaFlagsSP
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app
@@ -15,6 +17,10 @@ from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.")
SNG_HACK_UNAVAILABLE = tr_noop("sunnypilot Longitudinal Control must be available and enabled for your vehicle to use this feature.")
BSM_UNAVAILABLE = tr_noop("Enhanced Blind Spot Monitoring requires a TSS2 Toyota/Lexus without SecOC.")
BSM_STOCK_LONG_UNAVAILABLE = tr_noop("Enhanced Blind Spot Monitoring is not available while Enforce Factory Longitudinal Control is enabled.")
BSM_REQUIRED = tr_noop("This vehicle does not report blind spot status over the standard CAN signals. " +
"Enhanced Blind Spot Monitoring is required for blind spot detection.")
DESCRIPTIONS = {
'enforce_stock_longitudinal': tr_noop(
@@ -23,6 +29,10 @@ DESCRIPTIONS = {
'stop_and_go_hack': tr_noop(
'sunnypilot will allow some Toyota/Lexus cars to auto resume during stop and go traffic. ' +
'This feature is only applicable to certain models that are able to use longitudinal control. This is an alpha feature. Use at your own risk.'
),
'enhanced_bsm': tr_noop(
'sunnypilot will poll the blind spot monitor diagnostics directly from the radar. Use this on vehicles whose blind spot monitors ' +
'do not report blind spot status over the standard CAN signals. This is an alpha feature. Use at your own risk.'
)
}
@@ -47,9 +57,18 @@ class ToyotaSettings(BrandSettings):
enabled=lambda: not ui_state.engaged,
)
self.enhanced_bsm = toggle_item_sp(
lambda: tr("Enhanced Blind Spot Monitoring (Alpha)"),
description=lambda: tr(DESCRIPTIONS["enhanced_bsm"]),
initial_state=ui_state.params.get_bool("ToyotaEnhancedBsm"),
callback=self._on_enable_enhanced_bsm,
enabled=lambda: not ui_state.engaged,
)
self.items = [
self.enforce_stock_longitudinal,
self.stop_and_go_hack,
self.enhanced_bsm,
]
def _on_enable_enforce_stock_longitudinal(self, state: bool):
@@ -94,6 +113,25 @@ class ToyotaSettings(BrandSettings):
ui_state.params.put_bool("ToyotaStopAndGoHack", False)
ui_state.params.put_bool("OnroadCycleRequested", True)
def _on_enable_enhanced_bsm(self, state: bool):
if state:
def confirm_callback(result: int):
if result == DialogResult.CONFIRM:
ui_state.params.put_bool("ToyotaEnhancedBsm", True)
ui_state.params.put_bool("OnroadCycleRequested", True)
else:
self.enhanced_bsm.action_item.set_state(False)
content = (f"<h1>{self.enhanced_bsm.title}</h1><br>" +
f"<p>{self.enhanced_bsm.description}</p>")
dlg = ConfirmDialog(content, tr("Enable"), rich=True, callback=confirm_callback)
gui_app.push_widget(dlg)
else:
ui_state.params.put_bool("ToyotaEnhancedBsm", False)
ui_state.params.put_bool("OnroadCycleRequested", True)
def update_settings(self):
if ui_state.CP is not None:
longitudinal = ui_state.CP.openpilotLongitudinalControl
@@ -113,9 +151,36 @@ class ToyotaSettings(BrandSettings):
self.stop_and_go_hack.set_description(new_desc)
if show_desc:
self.stop_and_go_hack.show_description(True)
# Enhanced BSM needs the radar diagnostic address, which is only in the TX allowlist for
# TSS2 cars without SecOC that are not enforcing factory longitudinal control
bsm_supported = bool(ui_state.CP.flags & ToyotaFlags.TSS2) and not bool(ui_state.CP.flags & ToyotaFlags.SECOC)
bsm_available = bsm_supported and not enforce_stock
self.enhanced_bsm.action_item.set_enabled(bsm_available and not ui_state.engaged)
bsm_desc = tr(DESCRIPTIONS["enhanced_bsm"])
if not bsm_supported:
self.enhanced_bsm.action_item.set_state(False)
bsm_desc = "<b>" + tr(BSM_UNAVAILABLE) + "</b>\n\n" + bsm_desc
elif enforce_stock:
self.enhanced_bsm.action_item.set_state(False)
bsm_desc = "<b>" + tr(BSM_STOCK_LONG_UNAVAILABLE) + "</b>\n\n" + bsm_desc
elif ui_state.CP_SP is not None and ui_state.CP_SP.flags & ToyotaFlagsSP.SP_NEED_DEBUG_BSM:
bsm_desc = "<b>" + tr(BSM_REQUIRED) + "</b>\n\n" + bsm_desc
if self.enhanced_bsm.description != bsm_desc:
self.enhanced_bsm.set_description(bsm_desc)
if not bsm_available:
self.enhanced_bsm.show_description(True)
else:
self.stop_and_go_hack.action_item.set_enabled(False)
new_desc = "<b>" + tr(ONROAD_ONLY_DESCRIPTION) + "</b>\n\n" + tr(DESCRIPTIONS["stop_and_go_hack"])
if self.stop_and_go_hack.description != new_desc:
self.stop_and_go_hack.set_description(new_desc)
self.stop_and_go_hack.show_description(True)
self.enhanced_bsm.action_item.set_enabled(False)
bsm_desc = "<b>" + tr(ONROAD_ONLY_DESCRIPTION) + "</b>\n\n" + tr(DESCRIPTIONS["enhanced_bsm"])
if self.enhanced_bsm.description != bsm_desc:
self.enhanced_bsm.set_description(bsm_desc)
self.enhanced_bsm.show_description(True)
@@ -257,7 +257,6 @@ class ModelsLayoutMici(NavScroller):
count = 0
verifying = False
canceling = False
speed = 0.0
for model in manager.selectedBundle.models:
count += 1
p = model.artifact.downloadProgress
@@ -266,7 +265,6 @@ class ModelsLayoutMici(NavScroller):
progress += p.progress
verifying = verifying or p.status == custom.ModelManagerSP.DownloadStatus.verifying
canceling = canceling or p.canceling
speed = max(speed, p.speed) # only one file downloads at a time, so the non-zero speed is the live one
elif p.status in (custom.ModelManagerSP.DownloadStatus.downloaded,
custom.ModelManagerSP.DownloadStatus.cached):
progress += 100.0
@@ -281,10 +279,7 @@ class ModelsLayoutMici(NavScroller):
self.current_model_info.current_model_text.set_text(name_text)
self.current_model_info.info_header.set_text(tr("progress") + self._download_progress)
self.current_model_info.info_header._shimmer = True
progress_text = f"{progress/count:.2f}%"
if not verifying and not canceling:
progress_text += f" | {speed / 1024 ** 2:.1f} MB/s" if speed > 0 else " | --.- MB/s"
self.current_model_info.info_text.set_text(progress_text)
self.current_model_info.info_text.set_text(f"{progress/count:.2f}%")
elif manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloaded:
self.current_model_info.info_header.set_text(tr("downloaded"))
@@ -64,7 +64,6 @@ class UIStateSP:
self.enforce_torque_control: bool = False
self.custom_torque_params: bool = False
self.torque_override_enabled: bool = False
self.follow_distance_accel_boost: bool = False
self._sp_initialized: bool = False
def update(self) -> None:
@@ -178,7 +177,6 @@ class UIStateSP:
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")
self.follow_distance_accel_boost = self.params.get_bool("FollowDistanceAccelBoost")
self.torque_override_lat_accel_factor = float(self.params.get("TorqueParamsOverrideLatAccelFactor", return_default=True))
self.torque_override_friction = float(self.params.get("TorqueParamsOverrideFriction", return_default=True))
self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI")
@@ -7,7 +7,6 @@ See the LICENSE.md file in the root directory for more details.
"""
import argparse
import dataclasses
import inspect
import itertools
import mmap
import re
@@ -46,11 +45,6 @@ def _address(v):
return _root(v.src[0]), add + start, size
def _buffer(u):
from tinygrad.uop.ops import Ops
return u.buffer if u.op is Ops.BUFFER else getattr(u.arg, "buffer", None)
def _patched(buf, topo) -> tuple[bytearray, dict]:
from tinygrad.uop.ops import Ops
blob, rows = bytearray(buf.max_numel() * buf.dtype.itemsize), {}
@@ -78,7 +72,7 @@ def _evaluate(u, env):
table, index = _root(u.src[0].src[0]), _evaluate(u.src[0].src[1], env)
return struct.unpack_from(u.dtype.fmt, table.arg, index * u.dtype.itemsize)[0] if table.op is Ops.BINARY and index is not None else None
if u.op in GroupOp.ALU and None not in (values := [_evaluate(x, env) for x in u.src]):
return exec_alu(u.op, u.dtype, values, *([False] if "truncate_output" in inspect.signature(exec_alu).parameters else []))
return exec_alu(u.op, u.dtype, values, False)
return None
@@ -118,7 +112,7 @@ def convert(jit, kwargs: dict, frame: dict, target) -> tuple[dict, memoryview]:
from tinygrad.uop.ops import Ops
submit = jit.captured._linear.src[0].without_after
kernels, topo = submit.arg.aux.kernels, list(jit.captured._linear.toposort(enter_calls=True))
allocs = {str(name): u for u in topo if u.op is Ops.ALLOC and (name := getattr(u, "tag", None) or u.arg.name) is not None}
allocs = {str(u.tag): u for u in topo if u.op is Ops.ALLOC and u.tag is not None}
(commands, patched), kernargs = _patched(allocs["cmdbuf_compute_0"], topo), allocs["kernargs_compute_0"]
argument_words, pokes = _patched(kernargs, topo)[1], _input_pokes(submit, kernargs)
sh = {name: getattr(amd_gpu, f"reg{name}") + amd_gpu.GC_BASE__INST0_SEG0 - pm4.PACKET3_SET_SH_REG_START
@@ -164,7 +158,7 @@ def convert(jit, kwargs: dict, frame: dict, target) -> tuple[dict, memoryview]:
args.append({"input": pokes[slot][0]})
elif slot in argument_words:
base, at, nbytes = _address(argument_words[slot])
args.append({"buffer": buffers.setdefault(base, len(buffers)), "offset": at, "nbytes": nbytes or base.max_numel() * base.dtype.itemsize, "dtype": u8})
args.append({"buffer": buffers.setdefault(base, len(buffers)), "offset": at, "nbytes": nbytes or base.arg.buffer.nbytes, "dtype": u8})
outs, ins = kernel[6]
calls.append({"kind": "program", "name": re.sub(r"\x1b\[[0-9;]*m", "", kernel[1]), "lib": programs[image_alloc], "target": dataclasses.asdict(target),
"signature": [[f"data{i}", i, u8, [a.get("nbytes", 1)]] for i, a in enumerate(args)], "global_size": grid, "local_size": local,
@@ -172,13 +166,12 @@ def convert(jit, kwargs: dict, frame: dict, target) -> tuple[dict, memoryview]:
needs_init |= {args[i]["buffer"] for i in ins if "buffer" in args[i] and args[i]["buffer"] not in written}
written |= {args[i]["buffer"] for i in outs if "buffer" in args[i]}
assert all(_buffer(b) is not None for b, i in buffers.items() if i in needs_init), "a kernel reads a scratch before any kernel writes it"
names = list(jit.captured.expected_names)
inputs = [{"slot": slot, "name": names[slot] if slot < len(names) else None, "dtype": repr(u.dtype), "numel": int(u.max_numel()), "device": u.device}
for slot, u in enumerate(_prepare_jit_inputs((), kwargs)[0])]
header = {"input_names": names, "calls": calls, "returns": {"kind": "NoneType", "tensors": []}, "inputs": inputs,
"buffers": [{"nbytes": bb.nbytes if (bb := _buffer(b)) else b.max_numel() * b.dtype.itemsize, "device": bb.device if bb else b.device, "dtype": u8}
| ({"init": blob.add(bb.ensure_allocated().as_memoryview())} if i in needs_init else {}) for b, i in buffers.items()],
"buffers": [{"nbytes": b.arg.buffer.nbytes, "device": b.arg.buffer.device, "dtype": u8}
| ({"init": blob.add(b.arg.buffer.ensure_allocated().as_memoryview())} if i in needs_init else {}) for b, i in buffers.items()],
"check": {"inputs": [[path, store(array), device] for path, array, device in frame["inputs"]],
"outputs": [store(array) for array in frame["outputs"]], "output_container": frame["output_container"]}}
with sink:
+3 -9
View File
@@ -250,12 +250,6 @@ def _allocate_buffers(header: dict, blob: memoryview, device_map: dict | None, s
return buffers
def _fields(what, /, **values): # a tinygrad dataclass built or replaced with the fields it has today: a dropped field is fine, a new required one is not
known = {field.name for field in dataclasses.fields(what)}
values = {name: value for name, value in values.items() if name in known}
return what(**values) if isinstance(what, type) else dataclasses.replace(what, **values)
def _program_uop(call: dict, lib: bytes):
from tinygrad.helpers import Target
from tinygrad.uop.ops import KernelInfo, Ops, ProgramInfo, UOp
@@ -263,9 +257,9 @@ def _program_uop(call: dict, lib: bytes):
for name, compact_slot, dtype, shape in call["signature"]:
parameter = UOp.param(call["globals"][compact_slot], _dtype(dtype), math.prod(shape))
image = (shape[0], shape[1]) if len(shape) == 3 and shape[2] == 4 else None
parameters.append(parameter.replace(arg=_fields(parameter.arg, name=name, image=image)))
info = _fields(ProgramInfo, global_size=tuple(call["global_size"]), local_size=tuple(call["local_size"]), globals=tuple(call["globals"]),
outs=tuple(call["outs"]), ins=tuple(call["ins"]), target=_fields(Target, **call["target"]))
parameters.append(parameter.replace(arg=dataclasses.replace(parameter.arg, name=name, image=image)))
info = ProgramInfo(global_size=tuple(call["global_size"]), local_size=tuple(call["local_size"]), vars=(), globals=tuple(call["globals"]),
outs=tuple(call["outs"]), ins=tuple(call["ins"]), target=Target(**call["target"]))
return UOp(Ops.PROGRAM, src=(UOp(Ops.SINK, arg=KernelInfo(name=call["name"])), UOp(Ops.LINEAR, src=tuple(parameters)),
UOp(Ops.SOURCE, arg=""), UOp(Ops.BINARY, arg=lib)), arg=info)
@@ -23,7 +23,7 @@ def record_kernel_linear():
recorded, hcq_compile, compile_linear = [], realize.hcq_compile, jit.compile_linear
def recording_compile_linear(*args, **kwargs):
with mock.patch.object(realize, "hcq_compile", lambda linear, *a, **k: (linear.src and recorded.append(linear), hcq_compile(linear, *a, **k))[1]):
with mock.patch.object(realize, "hcq_compile", lambda linear, *a, **k: (recorded.append(linear), hcq_compile(linear, *a, **k))[1]):
return compile_linear(*args, **kwargs)
with mock.patch.object(jit, "compile_linear", recording_compile_linear):
@@ -38,18 +38,6 @@ def capture_frame(jit, kwargs: dict, output_container: str | None = None):
return {"inputs": inputs, "outputs": [tensor.numpy().copy() for tensor in outputs], "output_container": output_container}, result
def kernel_signature(ast, elf) -> list:
from tinygrad.dtype import AddrSpace
if hasattr(ast, "kernel_params"):
if any(p.arg.addrspace is AddrSpace.ALU for p in ast.kernel_params):
raise ValueError(f"{elf.name} takes a variable, which plain artifacts don't support")
compact = {slot: i for i, slot in enumerate(ast.arg.globals)}
return [[p.arg.name, compact[p.arg.slot], repr(p.dtype), [int(x) for x in p._shape]] for p in ast.kernel_params]
if not all(isinstance(slot, int) for _, slot, _, _ in elf.signature):
raise ValueError("TinyELF.signature lost its slots and this tinygrad has no UOp.kernel_params: update kernel_signature")
return [[name, slot, repr(dtype), [int(x) for x in shape]] for name, slot, dtype, shape in elf.signature]
def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict, memoryview]:
from tinygrad.uop.ops import Ops
sink = tempfile.TemporaryFile(dir=".")
@@ -63,12 +51,6 @@ def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict,
if uop.op is Ops.PARAM:
input_specs.setdefault(uop.arg.slot, {"dtype": repr(uop.dtype), "numel": int(uop.max_numel()), "device": uop.device})
return {"input": uop.arg.slot}
root, offset = uop, 0
while root.op in (Ops.AFTER, Ops.BITCAST, Ops.SHRINK):
offset, root = offset + (root.src[1].arg * root.src[0].dtype.itemsize if root.op is Ops.SHRINK else 0), root.src[0]
if root.op is Ops.ALLOC:
base = bases.setdefault(id(root), (len(bases), root, root.dtype))
return {"buffer": base[0], "offset": offset, "nbytes": int(uop.max_numel()) * uop.dtype.itemsize, "dtype": repr(uop.dtype)}
buffer = uop.buffer
root = buffer._base if buffer._base is not None else buffer
base = bases.setdefault(id(root), (len(bases), root, uop.base.dtype if buffer._base is None else uop.dtype))
@@ -81,7 +63,7 @@ def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict,
elf, info = ast.to_elf(), ast.arg
global_size, local_size = info.launch_dims({})
calls.append({"kind": "program", "name": elf.name, "lib": blob.add(bytes(elf.lib)), "target": dataclasses.asdict(elf.target),
"signature": kernel_signature(ast, elf),
"signature": [[name, slot, repr(dtype), [int(x) for x in shape]] for name, slot, dtype, shape in elf.signature],
"global_size": [int(x) for x in global_size], "local_size": [int(x) for x in local_size],
"globals": list(info.globals), "outs": list(info.outs), "ins": list(info.ins), "args": arguments})
outs, ins = info.outs, info.ins
@@ -95,9 +77,7 @@ def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict,
ret = jit.captured.ret
returns = {"kind": type(ret).__name__, "tensors": [describe(t.uop.base) | {"shape": [int(x) for x in t.shape]} for t in flatten(ret)]}
from tinygrad.device import Buffer
assert not [base for index, base, _ in bases.values() if index in needs_init and not isinstance(base, Buffer)], "a kernel reads a scratch before writing it"
buffers = [{"nbytes": base.nbytes if isinstance(base, Buffer) else int(base.max_numel()) * dtype.itemsize, "device": base.device, "dtype": repr(dtype)}
buffers = [{"nbytes": base.nbytes, "device": base.device, "dtype": repr(dtype)}
| ({"init": blob.add(base.as_memoryview())} if index in needs_init else {}) for index, base, dtype in bases.values()]
names = list(jit.captured.expected_names)
if check is not None:
@@ -1,121 +0,0 @@
"""
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 os
import re
import sys
import tempfile
import unittest
from unittest import mock
import numpy as np
os.environ.setdefault("CHECK_OOB", "0")
os.environ.setdefault("GMMU", "0")
if sys.platform == "darwin" and os.environ.get("DEV", "").startswith("MOCKUSB"): # the mock's VRAM is a memfd, which macOS lacks: an unlinked file is the same
from tinygrad.runtime.autogen import libc
def _memfd_create(name, flags):
fd, path = tempfile.mkstemp()
os.unlink(path)
return fd
libc.memfd_create = _memfd_create
from openpilot.common.test import OpenpilotTestCase
from openpilot.sunnypilot.modeld_v2 import convert_upstream_pkl
from openpilot.sunnypilot.modeld_v2.convert_upstream_pkl import convert
from openpilot.sunnypilot.modeld_v2.helpers import PlainJit, load_model
from openpilot.sunnypilot.modeld_v2.plain_artifact_compiler import capture_frame
IMG, DESIRE, STATE, OUT = (6, 8, 8), (8,), (4, 16), (1, 32)
class TestConvertUpstreamPkl(OpenpilotTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
if not os.environ.get("TINYGRAD_TESTS"):
raise unittest.SkipTest("TINYGRAD_TESTS not set. skipping converter test")
from tinygrad import Device, Tensor
assert Device.DEFAULT == "AMD" and getattr(Device["AMD"], "is_usb", False), f"{Device.DEFAULT} is not an AMD device behind the USB bridge"
assert (Tensor([1, 2, 3]) * 2).tolist() == [2, 4, 6]
cls.tmp = tempfile.TemporaryDirectory()
cls.rng = np.random.default_rng(0)
cls.weights = Tensor(cls.rng.standard_normal((16, 32)).astype(np.float32)).realize()
cls.bias = Tensor(cls.rng.standard_normal((1, 32)).astype(np.float32)).realize()
cls.input_specs = {"input_imgs": (IMG, "uint8", Device.DEFAULT), "desire": (DESIRE, "float32", Device.DEFAULT),
"features": (STATE, "float32", Device.DEFAULT)}
cls.output_specs = {"outputs": (OUT, "float32", Device.DEFAULT), "next_features": (STATE, "float32", Device.DEFAULT)}
@classmethod
def tearDownClass(cls):
cls.tmp.cleanup()
super().tearDownClass()
def model(self, input_imgs, desire, features, output_buffers):
from tinygrad import Tensor
feat = features.mean(axis=0).reshape(1, 16) + input_imgs.float().mean() + desire.sum()
Tensor.realize(output_buffers["outputs"].assign((feat @ self.weights + self.bias).relu()),
output_buffers["next_features"].assign(features[1:].cat(feat * 2)))
def inputs(self, seed: int) -> dict:
from examples.openpilot.helpers import allocate_inputs
rng = np.random.default_rng(seed)
def initialize(arrays: dict):
for value in arrays.values():
value[...] = rng.standard_normal(value.shape) if value.dtype.kind == "f" else rng.integers(0, 256, value.shape)
return allocate_inputs(self.input_specs, initialize) | {"output_buffers": allocate_inputs(self.output_specs)}
def jit(self):
from tinygrad import TinyJit
run = TinyJit(self.model, prune=True)
kwargs = self.inputs(1)
run(**kwargs)
run(**kwargs)
return run
def assert_matches_eager(self, plain, seed: int):
fresh, expected = self.inputs(seed), self.inputs(seed)
plain(**fresh)
self.model(**expected)
for name in self.output_specs:
assert np.array_equal(fresh["output_buffers"][name].numpy(), expected["output_buffers"][name].numpy()), name
def convert(self, jit) -> tuple[dict, memoryview]:
from tinygrad import Device
kwargs = self.inputs(2)
frame, _ = capture_frame(jit, kwargs, "output_buffers")
header, blob = convert(jit, kwargs, frame, Device[Device.DEFAULT].renderer.target)
kernels = jit.captured._linear.src[0].without_after.arg.aux.kernels
assert [call["name"] for call in header["calls"]] == [re.sub(r"\x1b\[[0-9;]*m", "", kernel[1]) for kernel in kernels]
assert all(re.fullmatch(r"\w+", call["name"]) for call in header["calls"]), "kernel names: failure point 1"
assert [(tuple(call["outs"]), tuple(call["ins"])) for call in header["calls"]] == [tuple(kernel[6]) for kernel in kernels], "outs/ins: failure point 1"
bound = {argument["input"] for call in header["calls"] for argument in call["args"] if "input" in argument}
assert bound == {spec["slot"] for spec in header["inputs"]}, f"input slots bound by pokes {sorted(bound)}: failure point 4"
assert [spec["name"] for spec in header["inputs"]][:len(self.input_specs)] == sorted(self.input_specs)
return header, blob
def test_converted_jit_matches_eager(self): # the submit decoded into plain calls, replayed against its own frame
jit = self.jit()
plain = PlainJit(*self.convert(jit))
self.assert_matches_eager(plain, 3)
self.assert_matches_eager(plain, 4)
def test_pickle_round_trip_through_main(self):
from examples.openpilot.helpers import dump_pickle, load_pickle
from tinygrad.engine.realize import lower_and_compile
pkl, out = os.path.join(self.tmp.name, "tiny_tinygrad.pkl"), os.path.join(self.tmp.name, "tiny_plain.pkl")
dump_pickle({"run": self.jit(), "input_specs": self.input_specs, "output_specs": self.output_specs, "metadata": {"name": "tiny"}}, pkl)
artifact = load_pickle(pkl, out_of_band=True)
artifact["run"].captured._linear = lower_and_compile(artifact["run"].captured._linear)
self.assert_matches_eager(PlainJit(*self.convert(artifact["run"])), 5)
with mock.patch.object(sys, "argv", ["convert_upstream_pkl", pkl, out]):
convert_upstream_pkl.main()
bundle = load_model(out, cache=False, verify=True)
assert bundle["metadata"] == {"name": "tiny"} and bundle["input_specs"] == self.input_specs and bundle["output_specs"] == self.output_specs
self.assert_matches_eager(bundle["run"], 6)
@@ -1,162 +0,0 @@
"""
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 dataclasses
import functools
import inspect
import os
import sys
import tempfile
from unittest import mock
import numpy as np
import unittest
import openpilot.sunnypilot.modeld_v2.helpers as helpers
from openpilot.common.test import OpenpilotTestCase
from openpilot.sunnypilot.modeld_v2 import compile_warp
from openpilot.sunnypilot.modeld_v2.frame_fusion import frame_part, pack_layout
from openpilot.sunnypilot.modeld_v2.helpers import PlainJit, cache_is_fresh, load_model, read_parts, write_bundle
from openpilot.sunnypilot.modeld_v2.plain_artifact_compiler import capture_frame, record_kernel_linear, serialize_jit
CAM_W, CAM_H, MODEL_W, MODEL_H = 32, 16, 16, 8
FRAME = f"{CAM_W},{CAM_H},{CAM_W},{CAM_H},{CAM_H // 2},{CAM_W * CAM_H * 3 // 2}"
FRAME_SIZE = CAM_W * CAM_H * 3 // 2
HOST = {"desire": (8,), "traffic_convention": (2,)}
STATE, OUT = (4, 16), (1, 32)
def compile_jit(fn, inputs: dict, output_container: str | None = None) -> tuple[dict, memoryview]:
from tinygrad import TinyJit
run = TinyJit(fn, prune=True)
with record_kernel_linear() as recorded:
run(**inputs)
run(**inputs)
check, _ = capture_frame(run, inputs, output_container)
assert len(recorded) == 1, f"expected one captured kernel list, got {len(recorded)}"
return serialize_jit(run, recorded[0], check)
class TestPlainArtifact(OpenpilotTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
if not os.environ.get("TINYGRAD_TESTS"):
raise unittest.SkipTest("TINYGRAD_TESTS env var not set, skipping artifact test.")
from tinygrad import Tensor
cls.tmp = tempfile.TemporaryDirectory()
cls.rng = np.random.default_rng(0)
cls.weights = Tensor(cls.rng.standard_normal((16, 32)).astype(np.float32)).realize()
cls.bias = Tensor(cls.rng.standard_normal((1, 32)).astype(np.float32)).realize()
@classmethod
def tearDownClass(cls):
cls.tmp.cleanup()
super().tearDownClass()
def model(self, x, h): # a two input, one weight model: the shape of a policy without the size
return ((x @ self.weights + self.bias).relu() + h.mean()).contiguous()
def tensors(self, seed: int):
from tinygrad import Tensor
rng = np.random.default_rng(seed)
return {"x": Tensor(rng.standard_normal((1, 16)).astype(np.float32)).realize(), "h": Tensor(rng.standard_normal((4, 16)).astype(np.float32)).realize()}
def artifact(self, name: str = "model") -> str:
path = os.path.join(self.tmp.name, f"{name}_tinygrad.pkl")
if not os.path.exists(path):
write_bundle(path, {"run": compile_jit(self.model, self.tensors(1))}, {"metadata": {"name": name}})
return path
def test_round_trip_matches_jit(self): # compiler -> bundle -> PlainJit, replay verified, then fresh inputs bit exact with eager tinygrad
bundle = load_model(self.artifact(), cache=False, verify=True)
assert bundle["metadata"] == {"name": "model"} and isinstance(bundle["run"], PlainJit)
fresh = self.tensors(2)
assert np.array_equal(bundle["run"](**fresh).numpy(), self.model(**fresh).numpy())
def test_replay_check_catches_a_changed_kernel(self):
parts, extras = read_parts(self.artifact())
header, blob = parts["run"]
poisoned = dict(header, check=dict(header["check"], outputs=[header["check"]["inputs"][0][1]]))
with self.assertRaises(helpers.ArtifactMismatch):
PlainJit(poisoned, blob)
def test_cache_round_trip_holds_no_weights_and_is_rebuilt_when_stale(self):
path = self.artifact("cached")
expected = load_model(path, cache=False, verify=False)["run"](**self.tensors(3)).numpy()
load_model(path)
assert cache_is_fresh(path)
with open(f"{path}.cache", "rb") as file:
cache = file.read()
assert self.weights.numpy().tobytes() not in cache and len(cache) < os.path.getsize(path)
with mock.patch.object(helpers, "PlainJit", side_effect=AssertionError("a fresh cache must not rebuild")):
cached = load_model(path)
assert np.array_equal(cached["run"](**self.tensors(3)).numpy(), expected)
with mock.patch.object(helpers, "tinygrad_identity", return_value="another tinygrad"):
assert not cache_is_fresh(path)
assert isinstance(load_model(path)["run"], PlainJit)
assert cache_is_fresh(path)
assert not cache_is_fresh(path) # keyed on the tinygrad that wrote it, so the next boot after a bump rebuilds once
os.utime(path)
with mock.patch.object(helpers, "tinygrad_identity", return_value="another tinygrad"):
assert not cache_is_fresh(path)
def test_fused_with_warp_matches_warp_then_model(self):
from tinygrad import Tensor
warp_path = os.path.join(self.tmp.name, "warp_tinygrad.pkl")
with mock.patch.object(sys, "argv", ["compile_warp", "--frame", FRAME, "--warp-to", f"{MODEL_W}x{MODEL_H}", "--output", warp_path]):
compile_warp.main()
warp = load_model(warp_path, cache=False)["run"]
def native(new_img, desire, traffic_convention, state_feat_q, output_buffers):
feat = state_feat_q.mean(axis=0).reshape(1, 16) + new_img.float().mean() + desire.sum() + traffic_convention[0]
Tensor.realize(output_buffers["outputs"].assign((feat @ self.weights + self.bias).relu()),
output_buffers["next_state_feat_q"].assign(state_feat_q[1:].cat(feat * 2)))
def model_inputs(new_img):
return {"new_img": new_img} | {name: Tensor(np.ones(shape, np.float32) * 0.5).realize() for name, shape in HOST.items()} | \
{"state_feat_q": Tensor(self.rng.standard_normal(STATE).astype(np.float32)).realize(),
"output_buffers": {"outputs": Tensor.zeros(OUT).contiguous().realize(), "next_state_feat_q": Tensor.zeros(STATE).contiguous().realize()}}
frames = Tensor(self.rng.integers(0, 256, (2, FRAME_SIZE), dtype=np.uint8)).realize()
tfm = Tensor(np.tile(np.eye(3, dtype=np.float32), (2, 1, 1))).realize()
path = os.path.join(self.tmp.name, "native_tinygrad.pkl")
part = compile_jit(native, model_inputs(warp(input_frame=frames, M_inv=tfm).realize()), "output_buffers")
extras = {"input_specs": {"new_img": ((2, 6, MODEL_H // 2, MODEL_W // 2), "|u1", "CPU"), "state_feat_q": (STATE, "<f4", "CPU")}
| {name: (shape, "<f4", "CPU") for name, shape in HOST.items()},
"output_specs": {"outputs": (OUT, "<f4", "CPU"), "next_state_feat_q": (STATE, "<f4", "CPU")},
"metadata": {"output_shapes": {"outputs": OUT, "next_state_feat_q": STATE}}}
write_bundle(path, {"run": part}, extras)
fused = load_model(path, cache=False, extend=functools.partial(frame_part, warp_path))
assert fused["run"].input_names == ["host", "state_feat_q"]
offsets, packed = pack_layout(HOST, FRAME_SIZE)
host = np.zeros(packed, np.uint8)
inputs = model_inputs(None)
for name, shape in ({"tfm": (2, 3, 3)} | HOST).items():
np.ndarray(shape, np.float32, host, offsets[name])[...] = tfm.numpy() if name == "tfm" else inputs[name].numpy()
host[offsets["frames"]:] = frames.numpy().ravel()
fused["run"](host=Tensor(host, device="NPY"), state_feat_q=inputs["state_feat_q"], output_buffers=inputs["output_buffers"])
expected = model_inputs(warp(input_frame=frames, M_inv=tfm).realize()) | {k: inputs[k] for k in ("state_feat_q",)}
native(**expected)
for name in ("outputs", "next_state_feat_q"):
assert np.array_equal(inputs["output_buffers"][name].numpy(), expected["output_buffers"][name].numpy()), name
def test_tinygrad_api_the_loader_reads(self):
import tinygrad.engine.jit as jit
from tinygrad.device import Buffer, TinyELF
from tinygrad.helpers import Target
from tinygrad.uop.ops import ParamArg, ProgramInfo, UOp
fields = {cls: {f.name for f in dataclasses.fields(cls)} for cls in (ProgramInfo, ParamArg, Target)}
assert fields[ProgramInfo] >= {"global_size", "local_size", "globals", "outs", "ins", "target"}, "ProgramInfo changed: update helpers._program_uop"
assert fields[ParamArg] >= {"slot", "dtype", "name", "image"}, "ParamArg changed: update helpers._program_uop"
assert fields[Target] >= {"device", "renderer", "arch"}, "Target changed: update the 'target' key in plain_artifact_compiler.serialize_jit"
assert "signature" in getattr(TinyELF, "__annotations__", {}) or hasattr(UOp, "kernel_params"), \
"kernel signatures moved: update plain_artifact_compiler.serialize_jit"
assert callable(getattr(UOp, "param", None)) and callable(getattr(UOp, "from_buffer", None)), "UOp.param/from_buffer: update helpers.PlainJit"
assert callable(getattr(UOp, "store_call", None)) and callable(getattr(UOp, "call", None)), "UOp.call/store_call: update helpers.PlainJit"
assert callable(getattr(Buffer, "view", None)) and callable(getattr(Buffer, "ensure_allocated", None)), "Buffer.view: update helpers._upload"
assert callable(getattr(jit, "compile_linear", None)) and callable(getattr(jit, "_prepare_jit_inputs", None)), "engine.jit: update helpers._Jit"
assert "expected_names" in inspect.signature(jit.CapturedJit).parameters, "CapturedJit: update helpers.PlainJit and plain_artifact_compiler.serialize_jit"
+7 -29
View File
@@ -34,8 +34,6 @@ TRANSIENT_HTTP_STATUS = frozenset({408, 429, 500, 502, 503, 504})
REPORT_INTERVAL = 0.5 # seconds between progress publications
# byte ranges count encoded bytes but iter_content yields decoded ones, so ask for an unencoded body
NO_COMPRESSION = {"Accept-Encoding": "identity"}
SPEED_SMOOTHING = 0.7 # weight of the previous speed sample in the published speed
SPEED_FLOOR = 0.05 * 1024 ** 2 # bytes/s; anything slower would only display as 0.0 MB/s, so it reads as no rate
class DownloadCancelled(Exception):
@@ -156,7 +154,6 @@ class ModelManagerSP:
artifact.downloadProgress.status = source_artifact.downloadProgress.status
artifact.downloadProgress.progress = source_artifact.downloadProgress.progress
artifact.downloadProgress.eta = source_artifact.downloadProgress.eta
artifact.downloadProgress.speed = source_artifact.downloadProgress.speed
def _calculate_eta(self, filename: str, progress: float) -> int:
if filename not in self._download_start_times or progress <= 0:
@@ -170,43 +167,28 @@ class ModelManagerSP:
eta = total_estimated_time - elapsed_time
return max(1, int(eta)) # Return at least 1 second if download is ongoing
def _set_progress(self, artifact, status, progress: float, eta: int = 0, speed: float = 0.0) -> None:
def _set_progress(self, artifact, status, progress: float, eta: int = 0) -> None:
artifact.downloadProgress.status = status
artifact.downloadProgress.progress = progress
artifact.downloadProgress.eta = eta
artifact.downloadProgress.speed = speed
self._sync_artifact_progress(artifact)
self._report_status()
def _publish_progress(self, artifact, done_bytes: int, total: int, speed: float) -> None:
def _publish_progress(self, artifact, done_bytes: int, total: int) -> None:
# held at 99 until the file passes its hash check
progress = min(99.0, done_bytes / total * 100) if total > 0 else 0.0
if speed > 0 and total > 0:
eta = max(1, int((total - done_bytes) / speed))
else:
eta = self._calculate_eta(artifact.fileName, progress)
self._set_progress(artifact, custom.ModelManagerSP.DownloadStatus.downloading, progress, eta, speed)
eta = self._calculate_eta(artifact.fileName, progress)
self._set_progress(artifact, custom.ModelManagerSP.DownloadStatus.downloading, progress, eta)
async def _report_until_done(self, tasks: list[asyncio.Future], artifact, progress: _Progress, total: int,
started: tuple[int, float]) -> None:
async def _report_until_done(self, tasks: list[asyncio.Future], artifact, progress: _Progress, total: int) -> None:
pending = set(tasks)
last_bytes, last_time = started
speed = 0.0
while pending:
done, pending = await asyncio.wait(pending, timeout=REPORT_INTERVAL)
for task in done:
task.result() # re-raises a worker failure
if self._download_interrupted():
raise DownloadCancelled("Download cancelled")
now = time.monotonic()
done_bytes = progress.snapshot()
if (dt := now - last_time) > 0:
instant = max(0.0, (done_bytes - last_bytes) / dt) # bytes/s this tick; clamped since retried pieces can lower the byte count
speed = instant if speed == 0 else SPEED_SMOOTHING * speed + (1 - SPEED_SMOOTHING) * instant
if speed < SPEED_FLOOR: # a stall decays toward 0 without reaching it, and a near-0 speed overflows the eta
speed = 0.0
last_time, last_bytes = now, done_bytes
self._publish_progress(artifact, done_bytes, total, speed)
self._publish_progress(artifact, progress.snapshot(), total)
async def _download_file(self, url: str, path: str, artifact) -> None:
self._download_start_times[artifact.fileName] = time.monotonic()
@@ -217,13 +199,12 @@ class ModelManagerSP:
await loop.run_in_executor(None, _prepare_target, path, total)
progress = _Progress()
cancel = threading.Event()
started = progress.snapshot(), time.monotonic() # before any worker can land bytes
with ThreadPoolExecutor(max_workers=MAX_CONCURRENT_PIECES) as pool:
tasks = [loop.run_in_executor(pool, _fetch_piece, url, path, i, start, end, progress, cancel, self._block_size)
for i, (start, end) in enumerate(pieces)]
try:
await self._report_until_done(tasks, artifact, progress, total, started)
await self._report_until_done(tasks, artifact, progress, total)
except BaseException as e:
cancel.set()
if isinstance(e, DownloadCancelled):
@@ -266,7 +247,6 @@ class ModelManagerSP:
self._download_start_times.pop(artifact.fileName, None)
artifact.downloadProgress.status = status.failed
artifact.downloadProgress.eta = 0
artifact.downloadProgress.speed = 0
self._sync_artifact_progress(artifact)
if self.selected_bundle:
self.selected_bundle.status = status.failed
@@ -279,7 +259,6 @@ class ModelManagerSP:
os.remove(full_path)
artifact.downloadProgress.status = status.failed
artifact.downloadProgress.eta = 0
artifact.downloadProgress.speed = 0
self._sync_artifact_progress(artifact)
if self.selected_bundle:
self.selected_bundle.status = status.failed
@@ -322,7 +301,6 @@ class ModelManagerSP:
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached
artifact.downloadProgress.progress = 100
artifact.downloadProgress.eta = 0
artifact.downloadProgress.speed = 0
else:
seen_artifacts.add(artifact.fileName)
await self._process_artifact(artifact, destination_path)
@@ -6,7 +6,6 @@ See the LICENSE.md file in the root directory for more details.
"""
import asyncio
import concurrent.futures
import contextlib
import dataclasses
import hashlib
@@ -130,13 +129,6 @@ def threaded_server(handler):
thread.join()
class InlineExecutor(concurrent.futures.Executor):
def submit(self, fn, /, *args, **kwargs):
future: concurrent.futures.Future = concurrent.futures.Future()
future.set_result(fn(*args, **kwargs)) # the piece lands before the reporter starts
return future
class ManagerDownloadTestBase(OpenpilotTestCase):
def setUp(self):
super().setUp()
@@ -147,7 +139,7 @@ class ManagerDownloadTestBase(OpenpilotTestCase):
self.addCleanup(self._tmp.cleanup)
self.dest = self._tmp.name
for name, value in (('PIECE_SIZE', PIECE_SIZE), ('REPORT_INTERVAL', 0.05), ('PIECE_BACKOFF', 0.01), ('SPEED_FLOOR', 100)):
for name, value in (('PIECE_SIZE', PIECE_SIZE), ('REPORT_INTERVAL', 0.05), ('PIECE_BACKOFF', 0.01)):
self.enterContext(mock.patch.object(manager_module, name, value))
self.reported: list[float] = []
@@ -237,31 +229,6 @@ class TestManagerDownload(ManagerDownloadTestBase):
assert self.reported and self.reported == sorted(self.reported), "progress only rises"
assert max(self.reported) <= 99.0, "progress holds below 100 until the hash check"
assert artifact.fileName not in self.manager._download_start_times
assert artifact.downloadProgress.speed > 0, "a running download shows its speed"
def test_speed_counts_bytes_landed_before_the_first_report(self):
self.enterContext(mock.patch.object(manager_module, 'ThreadPoolExecutor', lambda max_workers: InlineExecutor()))
artifact = self.download_file()
assert artifact.downloadProgress.speed > 0
def test_eta_comes_from_speed(self):
artifact = self.make_artifact()
self.manager._publish_progress(artifact, done_bytes=400, total=1000, speed=100)
assert artifact.downloadProgress.eta == 6
def test_stalled_speed_drops_to_zero(self):
stall_event = self.stall(0)
seen_speed = threading.Event()
def release_once_zero(*args):
if self.artifact.downloadProgress.speed > 0:
seen_speed.set()
elif seen_speed.is_set():
stall_event.set()
self.pm.send.side_effect = release_once_zero
self.download_file()
assert self.server.stall_released_by_event is True, "the stalled speed never reached exactly 0"
assert self.artifact.downloadProgress.speed > 0, "the speed must recover once data flows again"
def test_pieces_download_in_parallel(self):
stall_event = self.stall(0)
@@ -336,8 +303,7 @@ class TestProcessArtifact(ManagerDownloadTestBase):
self.download_bundle(source)
assert ACTIVE_BUNDLE_KEYS[source] in store and ACTIVE_BUNDLE_KEYS[other] not in store
assert self.manager.selected_bundle.status == ds.downloaded
assert self.artifact.downloadProgress.progress == 100
assert self.artifact.downloadProgress.eta == 0 and self.artifact.downloadProgress.speed == 0
assert self.artifact.downloadProgress.progress == 100 and self.artifact.downloadProgress.eta == 0
assert self.read_path() == WHOLE_BODY
os.remove(self.path)
@@ -126,6 +126,7 @@ def initialize_params(params) -> list[dict[str, Any]]:
keys.extend([
"ToyotaEnforceStockLongitudinal",
"ToyotaStopAndGoHack",
"ToyotaEnhancedBsm",
])
return [{k: params.get(k, return_default=True)} for k in keys]
@@ -1,72 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.cereal import log
from openpilot.common.params import Params
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.controls.lib.accel_boost import AccelBoost, ACCEL_BOOST_MAX
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
SCALE_RELAXED = 0.3
SCALE_STANDARD = 0.5
SCALE_AGGRESSIVE = 0.7
def get_accel_boost_scale(personality) -> float:
if personality == log.LongitudinalPersonality.aggressive:
return SCALE_AGGRESSIVE
if personality == log.LongitudinalPersonality.relaxed:
return SCALE_RELAXED
return SCALE_STANDARD
def get_accel_boost_max(personality, extra_enabled: bool = True) -> float:
if not extra_enabled:
return ACCEL_BOOST_MAX
return ACCEL_BOOST_MAX * (1.0 + get_accel_boost_scale(personality))
class AccelBoostExt:
def __init__(self, stock: AccelBoost | None = None):
self._stock = stock if stock is not None else AccelBoost()
self._extra = AccelBoost()
self.params = Params()
self.frame = -1
self.enabled = self.params.get_bool("FollowDistanceAccelBoost")
self.stock_boost = 0.0
self.extra_boost = 0.0
@property
def total_boost(self) -> float:
return self._stock.total_boost + self.extra_boost
def _update_params(self) -> None:
if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0:
self.enabled = self.params.get_bool("FollowDistanceAccelBoost")
def update(self, sm, output_a_target_e2e, output_a_target_mpc, a_cruise):
self.frame += 1
self._update_params()
if self.enabled and sm['selfdriveState'].enabled:
scale = get_accel_boost_scale(sm['selfdriveState'].personality)
extra_output = self._extra.update(sm, output_a_target_e2e, output_a_target_mpc, a_cruise)
self.extra_boost = (extra_output - output_a_target_e2e) * scale
else:
self.extra_boost = 0.0
self._extra.total_boost = 0.0
self._extra.boost_this_override = 0.0
self._extra.boost_eligible = False
output = self._stock.update(sm, output_a_target_e2e, output_a_target_mpc, a_cruise)
self.stock_boost = output - output_a_target_e2e
return output
target = output_a_target_e2e + self.extra_boost
output = self._stock.update(sm, target, output_a_target_mpc, a_cruise)
self.stock_boost = output - target
return output
@@ -9,7 +9,6 @@ from openpilot.cereal import messaging, custom
from opendbc.car import structs
from openpilot.common.constants import CV
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX
from openpilot.sunnypilot.selfdrive.controls.lib.accel_boost_ext import AccelBoostExt
from openpilot.sunnypilot.selfdrive.controls.lib.e2e_alerts_helper import E2EAlertsHelper
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.smart_cruise_control import SmartCruiseControl
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist
@@ -33,16 +32,6 @@ class LongitudinalPlannerSP:
self.output_v_target = 0.
self.output_a_target = 0.
self._accel_boost: AccelBoostExt | None = None
@property
def accel_boost(self) -> AccelBoostExt:
assert self._accel_boost is not None
return self._accel_boost
@accel_boost.setter
def accel_boost(self, controller) -> None:
self._accel_boost = AccelBoostExt(controller)
def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]:
CS = sm['carState']
@@ -1,237 +0,0 @@
from types import SimpleNamespace
from unittest import mock
from openpilot.cereal import log
from openpilot.common.realtime import DT_MDL
from openpilot.common.test import OpenpilotTestCase
from openpilot.selfdrive.controls.lib.accel_boost import AccelBoost, ACCEL_BOOST_MAX, ACCEL_BOOST_PER_OVERRIDE
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
from openpilot.sunnypilot.selfdrive.controls.lib.accel_boost_ext import AccelBoostExt, get_accel_boost_max
from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlannerSP
E2E_ACCEL = 0.5
PLANNER_ACCEL = 2.0
STEPS_PER_OVERRIDE = round(ACCEL_BOOST_PER_OVERRIDE / (0.025 * DT_MDL))
EXPECTED_SCALE = {
log.LongitudinalPersonality.relaxed: 0.3,
log.LongitudinalPersonality.standard: 0.5,
log.LongitudinalPersonality.aggressive: 0.7,
}
class FakeParams:
def __init__(self, enabled=True):
self.enabled = enabled
def get_bool(self, key):
return self.enabled
class FakeSubMaster:
def __init__(self, enabled=True, gas_pressed=False, v_ego=15.0,
personality=log.LongitudinalPersonality.standard):
self._messages = {
'selfdriveState': SimpleNamespace(enabled=enabled, personality=personality),
'carState': SimpleNamespace(gasPressed=gas_pressed, vEgo=v_ego),
}
def __getitem__(self, key):
return self._messages[key]
def update(boost, sm, e2e=E2E_ACCEL):
return boost.update(sm, e2e, PLANNER_ACCEL, PLANNER_ACCEL)
class PlannerStub(LongitudinalPlannerSP):
def __init__(self):
self._accel_boost = None
class TestAccelBoostExt(OpenpilotTestCase):
def setUp(self):
super().setUp()
self.params = FakeParams()
patcher = mock.patch("openpilot.sunnypilot.selfdrive.controls.lib.accel_boost_ext.Params", return_value=self.params)
patcher.start()
self.addCleanup(patcher.stop)
def _boost_to_max(self, personality):
boost = AccelBoostExt()
sm = FakeSubMaster(personality=personality)
# Eligibility is latched on a frame without gas, then applied on the next override.
update(boost, sm)
for _ in range(10):
sm['carState'].gasPressed = True
for _ in range(STEPS_PER_OVERRIDE):
update(boost, sm)
sm['carState'].gasPressed = False
update(boost, sm)
return boost, sm
def _applied_extra(self, boost, sm):
update(boost, sm)
return boost.extra_boost
def test_off_matches_stock_boost_exactly(self):
self.params.enabled = False
sm = FakeSubMaster()
stock = AccelBoost()
ext = AccelBoostExt()
stock_out = update(stock, sm)
ext_out = update(ext, sm)
self.assertAlmostEqual(ext_out, stock_out)
for _ in range(10):
sm['carState'].gasPressed = True
for _ in range(STEPS_PER_OVERRIDE):
stock_out = update(stock, sm)
ext_out = update(ext, sm)
self.assertAlmostEqual(ext_out, stock_out)
sm['carState'].gasPressed = False
stock_out = update(stock, sm)
ext_out = update(ext, sm)
self.assertAlmostEqual(ext_out, stock_out)
self.assertAlmostEqual(ext.total_boost, stock.total_boost)
self.assertAlmostEqual(ext.stock_boost, stock_out - E2E_ACCEL)
self.assertEqual(ext.extra_boost, 0.0)
self.assertEqual(ext._extra.total_boost, 0.0)
def test_no_boost_without_gas(self):
ext = AccelBoostExt()
sm = FakeSubMaster(gas_pressed=False)
out = update(ext, sm)
self.assertEqual(ext.total_boost, 0.0)
self.assertEqual(ext._extra.total_boost, 0.0)
self.assertAlmostEqual(out, E2E_ACCEL)
def test_boost_requires_e2e_to_be_limiting(self):
ext = AccelBoostExt()
sm = FakeSubMaster(gas_pressed=False)
ext.update(sm, E2E_ACCEL, E2E_ACCEL, E2E_ACCEL)
sm['carState'].gasPressed = True
for _ in range(10):
ext.update(sm, E2E_ACCEL, E2E_ACCEL, E2E_ACCEL)
self.assertEqual(ext.total_boost, 0.0)
self.assertEqual(ext.stock_boost, 0.0)
self.assertEqual(ext.extra_boost, 0.0)
self.assertEqual(ext._extra.total_boost, 0.0)
def test_extra_layer_per_override_cap(self):
ext = AccelBoostExt()
sm = FakeSubMaster(gas_pressed=False)
update(ext, sm)
sm['carState'].gasPressed = True
for _ in range(1000):
update(ext, sm)
self.assertAlmostEqual(ext._extra.total_boost, ACCEL_BOOST_PER_OVERRIDE)
applied = self._applied_extra(ext, sm)
expected = ACCEL_BOOST_PER_OVERRIDE * EXPECTED_SCALE[log.LongitudinalPersonality.standard]
self.assertAlmostEqual(applied, expected)
def test_extra_scales_with_following_distance(self):
applied = {}
for personality in (log.LongitudinalPersonality.relaxed,
log.LongitudinalPersonality.standard,
log.LongitudinalPersonality.aggressive):
ext, sm = self._boost_to_max(personality)
sm['carState'].gasPressed = True
applied[personality] = self._applied_extra(ext, sm)
for personality, value in applied.items():
self.assertAlmostEqual(value, ACCEL_BOOST_MAX * EXPECTED_SCALE[personality])
self.assertLess(applied[log.LongitudinalPersonality.relaxed], applied[log.LongitudinalPersonality.standard])
self.assertLess(applied[log.LongitudinalPersonality.standard], applied[log.LongitudinalPersonality.aggressive])
def test_capnp_dynamic_enum_personality_scales(self):
# Real SubMaster messages expose personality as a capnp DynamicEnum; equality still matches the enum value.
msg = log.SelfdriveState.new_message()
msg.personality = log.LongitudinalPersonality.aggressive
ext, sm = self._boost_to_max(msg.personality)
sm['carState'].gasPressed = True
applied = self._applied_extra(ext, sm)
self.assertAlmostEqual(applied, ACCEL_BOOST_MAX * EXPECTED_SCALE[log.LongitudinalPersonality.aggressive])
def test_extra_feeds_into_stock_last(self):
ext, sm = self._boost_to_max(log.LongitudinalPersonality.standard)
sm['carState'].gasPressed = True
inputs = []
original_update = AccelBoost.update
def spy(boost, sm_, e2e, mpc, cruise):
inputs.append(e2e)
return original_update(boost, sm_, e2e, mpc, cruise)
with mock.patch.object(AccelBoost, "update", spy):
out = update(ext, sm)
# First call is our extra layer on the raw target, last call is stock on the boosted target.
self.assertAlmostEqual(inputs[0], E2E_ACCEL)
self.assertAlmostEqual(inputs[-1], E2E_ACCEL + ext.extra_boost)
self.assertAlmostEqual(out, inputs[-1] + ext.stock_boost)
def test_extra_is_added_on_top_of_stock_boost(self):
ext, sm = self._boost_to_max(log.LongitudinalPersonality.aggressive)
sm['carState'].gasPressed = True
out = update(ext, sm)
extra = out - E2E_ACCEL - ext.stock_boost
self.assertAlmostEqual(ext._stock.total_boost, ACCEL_BOOST_MAX)
self.assertAlmostEqual(ext.total_boost, ACCEL_BOOST_MAX + ext.extra_boost)
self.assertAlmostEqual(ext.stock_boost, ACCEL_BOOST_MAX)
self.assertAlmostEqual(ext.extra_boost, ACCEL_BOOST_MAX * EXPECTED_SCALE[log.LongitudinalPersonality.aggressive])
self.assertAlmostEqual(extra, ext.extra_boost)
def test_sp_planner_wraps_assigned_stock_controller(self):
planner = PlannerStub()
stock = AccelBoost()
planner.accel_boost = stock
self.assertIsInstance(planner.accel_boost, AccelBoostExt)
self.assertIs(planner.accel_boost._stock, stock)
def test_wraps_existing_stock_controller(self):
stock = AccelBoost()
sm = FakeSubMaster()
update(stock, sm)
sm['carState'].gasPressed = True
update(stock, sm)
ext = AccelBoostExt(stock)
self.assertIs(ext._stock, stock)
self.assertAlmostEqual(ext.total_boost, stock.total_boost)
def test_boost_bar_max_matches_profile_cap(self):
for personality, scale in EXPECTED_SCALE.items():
self.assertAlmostEqual(get_accel_boost_max(personality), ACCEL_BOOST_MAX * (1.0 + scale))
self.assertAlmostEqual(get_accel_boost_max(personality, extra_enabled=False), ACCEL_BOOST_MAX)
def test_total_boost_is_stock_accumulated_plus_extra_applied(self):
ext, sm = self._boost_to_max(log.LongitudinalPersonality.aggressive)
sm['carState'].gasPressed = True
update(ext, sm)
self.assertAlmostEqual(ext.total_boost, ext._stock.total_boost + ext.extra_boost)
def test_turning_toggle_off_restores_stock_only(self):
ext, sm = self._boost_to_max(log.LongitudinalPersonality.aggressive)
self.assertGreater(ext._extra.total_boost, 0.0)
self.params.enabled = False
sm['carState'].gasPressed = True
# Params are re-read every PARAMS_UPDATE_PERIOD; the extra layer must clear on the next poll.
for _ in range(int(PARAMS_UPDATE_PERIOD / DT_MDL) + 1):
out = update(ext, sm)
self.assertEqual(ext._extra.total_boost, 0.0)
self.assertEqual(ext.extra_boost, 0.0)
self.assertAlmostEqual(out, E2E_ACCEL + ext.stock_boost)
def test_relaxing_following_distance_applies_immediately(self):
ext, sm = self._boost_to_max(log.LongitudinalPersonality.aggressive)
sm['selfdriveState'].personality = log.LongitudinalPersonality.relaxed
sm['carState'].gasPressed = True
applied = self._applied_extra(ext, sm)
self.assertAlmostEqual(applied, ACCEL_BOOST_MAX * EXPECTED_SCALE[log.LongitudinalPersonality.relaxed])
@@ -10,6 +10,7 @@ from openpilot.cereal import custom, messaging
from opendbc.car.structs import car
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, UNSUPPORTED_LONGITUDINAL_CAR
from opendbc.car.subaru.values import CAR as SUBARU_CAR, SubaruFlags
from opendbc.car.toyota.values import CAR as TOYOTA_CAR, ToyotaFlags
from opendbc.sunnypilot.car.interfaces import has_bsm
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
from openpilot.common.params import Params
@@ -43,6 +44,7 @@ CAPABILITY_FIELDS = (
"device_type",
"subaru_has_sng",
"hyundai_alpha_long_available",
"toyota_enhanced_bsm_available",
)
CAPABILITY_LABELS: dict[str, str] = {
@@ -65,6 +67,7 @@ CAPABILITY_LABELS: dict[str, str] = {
"device_type": "Device type",
"subaru_has_sng": "Subaru Stop-and-Go available",
"hyundai_alpha_long_available": "Hyundai Alpha Longitudinal available",
"toyota_enhanced_bsm_available": "Toyota Enhanced BSM available",
}
# Explicit defaults for non-boolean capability fields
@@ -111,6 +114,16 @@ def _resolve_brand_capabilities(caps: dict, bundle_platform: str, CP) -> None:
caps["subaru_has_sng"] = not bool(CP.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
caps["has_stop_and_go"] = caps["subaru_has_sng"]
elif brand == "toyota":
if bundle_platform:
try:
flags = TOYOTA_CAR[bundle_platform].config.flags
caps["toyota_enhanced_bsm_available"] = bool(flags & ToyotaFlags.TSS2) and not bool(flags & ToyotaFlags.SECOC)
except KeyError:
cloudlog.exception(f"capabilities: unknown toyota platform {bundle_platform!r}")
elif CP is not None:
caps["toyota_enhanced_bsm_available"] = bool(CP.flags & ToyotaFlags.TSS2) and not bool(CP.flags & ToyotaFlags.SECOC)
def generate_capabilities(params: Params | None = None) -> dict:
"""Generate a SettingsCapabilities dict from CarParams + boolean params.
@@ -156,7 +156,7 @@ The tables below describe the **compiled** `settings_ui.json` schema — what th
**Enablement rules**: Grayed out (disabled) when rules fail. Frontend shows a contextual badge explaining why.
**Capability fields** (referenced in rules): `has_longitudinal_control`, `has_icbm`, `icbm_available`, `torque_allowed`, `brand`, `pcm_cruise`, `alpha_long_available`, `steer_control_type`, `has_bsm`, `is_release`, `is_sp_release`, `is_development`, `tesla_has_vehicle_bus`, `has_stop_and_go`, `stock_longitudinal`
**Capability fields** (referenced in rules): `has_longitudinal_control`, `has_icbm`, `icbm_available`, `torque_allowed`, `brand`, `pcm_cruise`, `alpha_long_available`, `steer_control_type`, `has_bsm`, `is_release`, `is_sp_release`, `is_development`, `tesla_has_vehicle_bus`, `has_stop_and_go`, `stock_longitudinal`, `toyota_enhanced_bsm_available`
---
+22 -13
View File
@@ -620,19 +620,6 @@
}
]
},
{
"key": "FollowDistanceAccelBoost",
"widget": "toggle",
"title": "Extra Acceleration Boost by Following Distance",
"description": "Adds extra acceleration on top of the stock gas-override boost. The extra boost follows the Following Distance setting, strongest when Aggressive and weakest when Relaxed. The stock boost remains active when this is off.",
"enablement": [
{
"type": "capability",
"field": "has_longitudinal_control",
"equals": true
}
]
},
{
"key": "IntelligentCruiseButtonManagement",
"widget": "toggle",
@@ -2337,6 +2324,28 @@
"equals": false
}
]
},
{
"key": "ToyotaEnhancedBsm",
"widget": "toggle",
"needs_onroad_cycle": true,
"title": "Enhanced Blind Spot Monitoring (Alpha)",
"description": "sunnypilot will poll the blind spot monitor diagnostics directly from the radar. Use this on vehicles whose blind spot monitors do not report blind spot status over the standard CAN signals. This is an alpha feature. Use at your own risk.",
"enablement": [
{
"type": "not_engaged"
},
{
"type": "capability",
"field": "toyota_enhanced_bsm_available",
"equals": true
},
{
"type": "capability",
"field": "stock_longitudinal",
"equals": false
}
]
}
]
}
@@ -30,14 +30,6 @@ sections:
label: Relaxed
enablement:
- $ref: '#/macros/longitudinal'
- key: FollowDistanceAccelBoost
widget: toggle
title: Extra Acceleration Boost by Following Distance
description: Adds extra acceleration on top of the stock gas-override boost. The extra boost follows
the Following Distance setting, strongest when Aggressive and weakest when Relaxed. The stock boost
remains active when this is off.
enablement:
- $ref: '#/macros/longitudinal'
- key: IntelligentCruiseButtonManagement
widget: toggle
title: Intelligent Cruise Button Management (ICBM) (Alpha)
@@ -101,3 +101,18 @@ sections:
- type: param
key: ToyotaEnforceStockLongitudinal
equals: false
- key: ToyotaEnhancedBsm
widget: toggle
needs_onroad_cycle: true
title: Enhanced Blind Spot Monitoring (Alpha)
description: sunnypilot will poll the blind spot monitor diagnostics directly from the radar. Use this on vehicles whose
blind spot monitors do not report blind spot status over the standard CAN signals. This is an alpha feature. Use at
your own risk.
enablement:
- $ref: '#/macros/not_engaged'
- type: capability
field: toyota_enhanced_bsm_available
equals: true
- type: capability
field: stock_longitudinal
equals: false
@@ -69,12 +69,18 @@ class TestOpaquePerBrandFlags(OpenpilotTestCase):
def test_hyundai_alpha_long_available_field_present(self):
assert "hyundai_alpha_long_available" in CAPABILITY_FIELDS
def test_toyota_enhanced_bsm_available_field_present(self):
assert "toyota_enhanced_bsm_available" in CAPABILITY_FIELDS
def test_subaru_has_sng_default_false(self, caps):
assert caps["subaru_has_sng"] is False
def test_hyundai_alpha_long_available_default_false(self, caps):
assert caps["hyundai_alpha_long_available"] is False
def test_toyota_enhanced_bsm_available_default_false(self, caps):
assert caps["toyota_enhanced_bsm_available"] is False
class TestCapabilitiesShape(OpenpilotTestCase):
def test_all_fields_present(self, caps):
@@ -207,6 +207,7 @@ class TestNotEngagedReplacement(OpenpilotTestCase):
"AlphaLongitudinalEnabled",
"ToyotaEnforceStockLongitudinal",
"ToyotaStopAndGoHack",
"ToyotaEnhancedBsm",
], names=["key"])
def test_offroad_only_replaced_with_not_engaged(self, schema, key):
"""These items should use not_engaged, not offroad_only."""
@@ -286,6 +286,7 @@ class TestKnownVehicleSettings(OpenpilotTestCase):
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("toyota"))}
assert "ToyotaEnforceStockLongitudinal" in keys
assert "ToyotaStopAndGoHack" in keys
assert "ToyotaEnhancedBsm" in keys
def test_tesla_has_coop_steering(self, schema):
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("tesla"))}