Compare commits

..

4 Commits

Author SHA1 Message Date
rav4kumar fb95b72e23 long: taper stopping decel instead of ramping to stopAccel 2026-10-20 11:14:50 -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
James Vecellio-Grant 922513f312 ci: lfs pointers survive gc (#2066)
* ci: lfs pointers survive gc

* comma got gc'ed

* format
2026-10-18 22:41:23 -07:00
18 changed files with 306 additions and 16 deletions
+79 -1
View File
@@ -33,7 +33,7 @@ jobs:
- name: LFS Fetch
run: |
git lfs fetch
git lfs fetch || echo "::warning::some objects are missing on commaai/openpilot-lfs; mirroring the rest"
- name: Set up Git
run: |
@@ -68,6 +68,7 @@ jobs:
git config lfs.activitytimeout 300
git config lfs.transfer.maxretries 5
git config lfs.concurrenttransfers 4
git config lfs.allowincompletepush true
- name: Push LFS
id: sync-and-commit
@@ -81,3 +82,80 @@ jobs:
done
echo "All push attempts failed"
exit 1
- name: Checkout sunnypilot
uses: actions/checkout@v4
with:
repository: 'sunnypilot/sunnypilot'
path: sunnypilot
fetch-depth: 0
filter: blob:limit=1k
lfs: false
- name: Reference objects on sunnypilot-lfs
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
run: |
set -euo pipefail
GIT_LFS_SKIP_SMUDGE=1 git clone --quiet https://huggingface.co/sunnypilot/sunnypilot-lfs hf-lfs
mkdir -p hf-lfs/all
cat > scan_pointers.py <<'EOF'
import re, subprocess, sys, threading
repo = sys.argv[1]
chk = subprocess.run(["git", "-C", repo, "cat-file", "--batch-all-objects",
"--batch-check=%(objectname) %(objecttype) %(objectsize)"],
capture_output=True, text=True, check=True).stdout
small = [f[0] for f in (l.split() for l in chk.splitlines()) if f[1] == "blob" and int(f[2]) < 1024]
p = subprocess.Popen(["git", "-C", repo, "cat-file", "--batch=%(objectsize)"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
def feed():
for sha in small:
p.stdin.write(sha.encode() + b"\n")
p.stdin.close()
threading.Thread(target=feed, daemon=True).start()
rx = re.compile(rb"^version https://git-lfs\.github\.com/spec/v1\noid sha256:([0-9a-f]{64})\nsize ([0-9]+)\n")
seen = {}
for _ in small:
n = int(p.stdout.readline()); body = p.stdout.read(n); p.stdout.read(1)
m = rx.match(body)
if m:
seen.setdefault(m.group(1).decode(), m.group(2).decode())
for oid, size in sorted(seen.items()):
print(oid, size)
EOF
for repo in . sunnypilot; do
python3 scan_pointers.py "$repo"
done | sort -u | while read -r oid size; do
[ -e "hf-lfs/all/$oid" ] && continue
printf 'version https://git-lfs.github.com/spec/v1\noid sha256:%s\nsize %s\n' "$oid" "$size" > "hf-lfs/all/$oid"
echo "$oid"
done > new_oids.txt
echo "new pointer files: $(wc -l < new_oids.txt)"
dropped=0
while mapfile -t -n 100 chunk && ((${#chunk[@]})); do
body=$(for oid in "${chunk[@]}"; do
size=$(awk '/^size /{print $2}' "hf-lfs/all/$oid")
printf '{"oid":"%s","size":%s}\n' "$oid" "$size"
done | jq -s '{operation:"download",transfers:["basic"],objects:.}')
for attempt in 1 2 3 4 5; do
resp=$(curl -sS -u "user:${HF_TOKEN}" -w '\n%{http_code}' \
-H 'Accept: application/vnd.git-lfs+json' -H 'Content-Type: application/vnd.git-lfs+json' \
--data "$body" "${LFS_URL}/objects/batch") && [ "$(tail -n1 <<<"$resp")" = 200 ] && break
echo "batch check attempt $attempt failed ($(tail -n1 <<<"$resp")), retrying in 60s"; sleep 60
done
[ "$(tail -n1 <<<"$resp")" = 200 ] || { echo "batch check failed"; exit 1; }
for oid in $(sed '$d' <<<"$resp" | jq -r '.objects[] | select(.error) | .oid'); do
rm -f "hf-lfs/all/$oid"; dropped=$((dropped+1))
done
done < new_oids.txt
echo "dropped (not on HF): $dropped"
cd hf-lfs
git add -A all
if git diff --cached --quiet; then
echo "nothing new to reference"
exit 0
fi
git commit -q -m "Reference $(git diff --cached --name-only | wc -l | tr -d ' ') new LFS objects"
git push origin HEAD:main
+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
View File
@@ -246,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"}},
@@ -9,6 +9,11 @@ CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N]
LongCtrlState = car.CarControl.Actuators.LongControlState
# Stopping is shaped as a constant-jerk taper so the brake is released as the car comes to rest.
# The old behavior ramped towards CP.stopAccel while still rolling, making the last meter the hardest.
STOPPING_JERK = 2.0 # m/s^3, rate the stopping decel is released at (higher eases off faster)
STOPPING_HOLD_SPEED = 0.02 # m/s, below this command CP.stopAccel to hold the car
def long_control_state_trans(CP_SP, active, long_control_state,
should_stop, brake_pressed, cruise_standstill):
@@ -51,6 +56,18 @@ class LongControl:
def reset(self):
self.pid.reset()
def _stopping_accel(self, v_ego: float) -> float:
v_ego = max(float(v_ego), 0.0)
if v_ego < STOPPING_HOLD_SPEED:
return float(self.CP.stopAccel)
# Decel that reaches zero at standstill with constant jerk, never stronger than what we already have
taper = -np.sqrt(2.0 * STOPPING_JERK * v_ego)
goal = min(0.0, max(self.last_output_accel, taper))
output_accel = np.clip(goal, self.last_output_accel - STOPPING_JERK * DT_CTRL,
self.last_output_accel + STOPPING_JERK * DT_CTRL)
return float(min(output_accel, 0.0))
def update(self, active, CS, a_target, should_stop, accel_limits):
"""Update longitudinal control. This updates the state machine and runs a PID loop"""
self.pid.neg_limit = accel_limits[0]
@@ -64,11 +81,7 @@ class LongControl:
output_accel = 0.
elif self.long_control_state == LongCtrlState.stopping:
output_accel = self.last_output_accel
if output_accel > self.CP.stopAccel:
output_accel = min(output_accel, 0.0)
# TODO: can we just go straight to stopAccel?
output_accel -= 1.0 * DT_CTRL # m/s^2/s while trying to stop
output_accel = self._stopping_accel(CS.vEgo)
self.reset()
else: # LongCtrlState.pid
@@ -1,6 +1,10 @@
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, long_control_state_trans
from openpilot.selfdrive.controls.lib.longcontrol import (LongControl, LongCtrlState, STOPPING_HOLD_SPEED,
STOPPING_JERK, long_control_state_trans)
class TestLongControlStateTransition(OpenpilotTestCase):
@@ -42,3 +46,75 @@ class TestLongControlStateTransition(OpenpilotTestCase):
next_state = long_control_state_trans(CP_SP, active, current_state,
should_stop=False, brake_pressed=False, cruise_standstill=False)
assert next_state == LongCtrlState.pid
class TestLongControlStopping(OpenpilotTestCase):
def _long_control(self, stop_accel=-2.0, entry_accel=-1.2, v_ego=0.3):
CP = car.CarParams.new_message()
CP.longitudinalTuning.kiBP = [0.0]
CP.longitudinalTuning.kiV = [0.0]
CP.stopAccel = stop_accel
lc = LongControl(CP, custom.CarParamsSP.new_message())
lc.long_control_state = LongCtrlState.stopping
lc.last_output_accel = entry_accel
CS = car.CarState.new_message()
CS.vEgo = float(v_ego)
CS.brakePressed = False
CS.cruiseState.standstill = False
return lc, CS
def _run_stop(self, lc, CS, frames=500):
accels = []
distance = 0.0
for _ in range(frames):
accels.append(lc.update(True, CS, 0.0, True, (-4.0, 2.0)))
if CS.vEgo < STOPPING_HOLD_SPEED:
break
distance += CS.vEgo * DT_CTRL
CS.vEgo = float(max(0.0, CS.vEgo + accels[-1] * DT_CTRL))
return accels, distance
def test_releases_braking_instead_of_ramping_to_stop_accel(self):
lc, CS = self._long_control(entry_accel=-1.2)
accels, _ = self._run_stop(lc, CS)
# the stop state must ease off the brake, not add more of it as the car slows
assert accels[0] > -1.2
assert accels[-2] > accels[0]
# and it must actually get gentle before standstill instead of stopping at the entry decel
assert accels[-2] > -0.8
def test_never_increases_decel_while_rolling(self):
lc, CS = self._long_control(entry_accel=-1.2)
accels, _ = self._run_stop(lc, CS)
rolling = accels[:-1]
assert all(b >= a - 1e-9 for a, b in zip(rolling[:-1], rolling[1:], strict=False)), rolling
def test_jerk_is_bounded(self):
lc, CS = self._long_control(entry_accel=-1.5)
accels, _ = self._run_stop(lc, CS)
rolling = accels[:-1]
max_step = max(abs(b - a) for a, b in zip(rolling[:-1], rolling[1:], strict=False))
assert max_step <= STOPPING_JERK * DT_CTRL + 1e-9
def test_holds_with_stop_accel_at_standstill(self):
lc, CS = self._long_control(stop_accel=-2.0)
accels, _ = self._run_stop(lc, CS)
assert CS.vEgo < STOPPING_HOLD_SPEED
assert accels[-1] == -2.0
def test_weak_entry_does_not_add_braking(self):
lc, CS = self._long_control(entry_accel=-0.2)
accels, _ = self._run_stop(lc, CS)
assert min(accels[:-1]) >= -0.2 - 1e-9
def test_never_commands_positive_accel(self):
lc, CS = self._long_control(entry_accel=0.5)
accels, _ = self._run_stop(lc, CS)
assert max(accels) <= 0.0
def test_stops_within_short_distance(self):
lc, CS = self._long_control(entry_accel=-1.2)
_, distance = self._run_stop(lc, CS)
assert CS.vEgo < STOPPING_HOLD_SPEED
assert distance < 0.25
@@ -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)
+1 -1
View File
@@ -258,7 +258,7 @@ def _program_uop(call: dict, lib: bytes):
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=dataclasses.replace(parameter.arg, name=name, image=image)))
info = ProgramInfo(global_size=tuple(call["global_size"]), local_size=tuple(call["local_size"]), globals=tuple(call["globals"]),
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)
@@ -39,7 +39,6 @@ def capture_frame(jit, kwargs: dict, output_container: str | None = None):
def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict, memoryview]:
from tinygrad.dtype import AddrSpace
from tinygrad.uop.ops import Ops
sink = tempfile.TemporaryFile(dir=".")
blob, bases, input_specs = Blob(sink), {}, {}
@@ -61,12 +60,10 @@ def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict,
for call in kernel_linear.src:
ast, arguments = call.src[0], [describe(argument) for argument in call.src[1:]]
if ast.op is Ops.PROGRAM:
elf, info, compact = ast.to_elf(), ast.arg, {slot: i for i, slot in enumerate(ast.arg.globals)}
if any(p.addrspace is AddrSpace.ALU for p in ast.kernel_params):
raise ValueError(f"{elf.name} takes a variable, which plain artifacts don't support")
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": [[p.arg.name, compact[p.arg.slot], repr(p.dtype), [int(x) for x in p._shape]] for p in ast.kernel_params],
"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
@@ -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]
@@ -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`
---
@@ -2324,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
}
]
}
]
}
@@ -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"))}