mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6934a2da8 | |||
| b9851e59fa | |||
| db987be5c8 | |||
| c5f6a854c5 | |||
| c99cca360d |
@@ -0,0 +1,154 @@
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
BASE = '1d30dc343469e93967d2de3afdc966901009ed5d'
|
||||
OUT = Path('bsm-results')
|
||||
OUT.mkdir(exist_ok=True)
|
||||
HOST = 'opendbc_repo/opendbc/car/hyundai/tests/test_ioniq6_bsm.py'
|
||||
NATIVE = 'opendbc_repo/opendbc/safety/tests/test_hyundai_ioniq6_long.py'
|
||||
|
||||
def run(*args):
|
||||
subprocess.run(args, check=True)
|
||||
|
||||
def qualify():
|
||||
controller = Path('opendbc_repo/opendbc/car/hyundai/carcontroller.py')
|
||||
shipping = controller.read_bytes()
|
||||
try:
|
||||
controller.write_bytes(subprocess.check_output(['git', 'show', BASE + ':' + str(controller)]))
|
||||
result = subprocess.run([sys.executable, '-m', 'pytest', '-q', HOST, '-k',
|
||||
'requires_a_matching_queued', '--junitxml=bsm-results/baseline.xml'])
|
||||
cases = list(ET.parse(OUT / 'baseline.xml').getroot().iter('testcase'))
|
||||
failures = [case for case in cases if case.find('failure') is not None]
|
||||
assert result.returncode == 1 and failures and not list(ET.parse(OUT / 'baseline.xml').iter('error'))
|
||||
assert all('UNQUEUED_BLINDSPOT_WARNING' in ''.join(case.find('failure').itertext()) for case in failures)
|
||||
(OUT / 'baseline.json').write_text(json.dumps({'expectedFailingCases': len(failures), 'confirmed': True}))
|
||||
finally:
|
||||
controller.write_bytes(shipping)
|
||||
assert controller.read_bytes() == shipping
|
||||
suites = [HOST, *map(str, sorted(Path('opendbc_repo/opendbc/car/hyundai/tests').glob('test_ioniq6_*.py'))),
|
||||
'openpilot/starpilot/lateral/tests/test_auto_lane_change.py',
|
||||
'openpilot/starpilot/lateral/tests/test_lane_change_defaults.py',
|
||||
'openpilot/starpilot/lateral/tests/test_lane_change_floor.py']
|
||||
run(sys.executable, '-m', 'pytest', '-q', '--junitxml=bsm-results/host.xml', *dict.fromkeys(suites))
|
||||
from tools.ci.run_vehicle_tests import build_safety_library
|
||||
native = [NATIVE,
|
||||
'opendbc_repo/opendbc/safety/tests/test_hyundai_canfd.py',
|
||||
'opendbc_repo/opendbc/safety/tests/test_hyundai_canfd_carnival.py',
|
||||
'opendbc_repo/opendbc/safety/tests/test_hyundai_canfd_torque_envelope.py',
|
||||
'opendbc_repo/opendbc/safety/tests/test_hyundai_ioniq6_stock_aol.py']
|
||||
for release in (False, True):
|
||||
variant = 'release' if release else 'debug'
|
||||
provenance = build_safety_library(release, OUT)
|
||||
(OUT / f'native-{variant}.json').write_text(json.dumps(provenance, indent=2))
|
||||
# A fresh interpreter avoids mixing the two C global-state libraries.
|
||||
loader = ('from opendbc.safety.tests.libsafety import libsafety_py; '
|
||||
f'libsafety_py.load({str((OUT / ("libsafety-" + variant + ".so")).resolve())!r}); '
|
||||
'import pytest,sys; sys.exit(pytest.main(sys.argv[1:]))')
|
||||
run(sys.executable, '-c', loader, '-q', f'--junitxml=bsm-results/native-{variant}.xml', *native)
|
||||
paths = [str(controller), HOST, NATIVE]
|
||||
run('.venv/bin/ruff', 'check', *paths)
|
||||
import re
|
||||
from collections import Counter
|
||||
def typing_result():
|
||||
result = subprocess.run(['.venv/bin/ty', 'check', '--output-format', 'concise', *paths], capture_output=True, text=True)
|
||||
output = result.stdout + result.stderr
|
||||
assert result.returncode in (0, 1) and ('error[' in output or result.returncode == 0), output
|
||||
return result.returncode, output, Counter(re.sub(r':\d+:\d+:', ':', line) for line in output.splitlines() if 'error[' in line)
|
||||
shipping_typing = typing_result()
|
||||
saved = {Path(name): Path(name).read_bytes() for name in paths}
|
||||
try:
|
||||
for path in saved:
|
||||
path.write_bytes(subprocess.check_output(['git', 'show', BASE + ':' + str(path)]))
|
||||
baseline_typing = typing_result()
|
||||
finally:
|
||||
for path, data in saved.items():
|
||||
path.write_bytes(data)
|
||||
(OUT / 'typing-shipping.txt').write_text(shipping_typing[1])
|
||||
(OUT / 'typing-baseline.txt').write_text(baseline_typing[1])
|
||||
assert not (shipping_typing[2] - baseline_typing[2]), shipping_typing[1]
|
||||
(OUT / 'typing.json').write_text(json.dumps({'introducedErrors': 0,
|
||||
'baselineErrors': sum(baseline_typing[2].values()), 'shippingErrors': sum(shipping_typing[2].values()),
|
||||
'baselineExit': baseline_typing[0], 'shippingExit': shipping_typing[0]}))
|
||||
(OUT / 'source.json').write_text(json.dumps({str(p): hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
for p in [controller, Path(HOST), Path(NATIVE), Path('opendbc_repo/opendbc/safety/modes/hyundai_canfd.h')]}, indent=2))
|
||||
|
||||
def firmware():
|
||||
variant = os.environ['BSM_VARIANT']
|
||||
names = subprocess.check_output(['git', 'ls-tree', '-r', '--name-only', 'HEAD', 'panda/board/obj'], text=True).splitlines()
|
||||
targets = [name for name in names if name.endswith(('.bin.signed', '.bin'))]
|
||||
targets += ['panda/board/obj/version', 'panda/board/obj/gitversion.h', 'panda/board/obj/cert.h']
|
||||
assert sum(name.endswith('.bin.signed') for name in targets) == 17
|
||||
with (OUT / 'firmware-build.log').open('w') as log:
|
||||
result = subprocess.run(['.venv/bin/scons', '-j2', '--verbose', *targets], stdout=log, stderr=subprocess.STDOUT)
|
||||
print((OUT / 'firmware-build.log').read_text()[-18000:])
|
||||
assert result.returncode == 0
|
||||
run(sys.executable, 'tools/ci/record_firmware.py', '--variant', variant, '--build-log',
|
||||
str(OUT / 'firmware-build.log'), '--output', str(OUT / 'firmware-provenance.json'))
|
||||
import base64
|
||||
import zlib
|
||||
key = base64.b64decode(Path('panda/board/crypto/certs/debug.pub').read_bytes().split()[1])
|
||||
fields = []
|
||||
for _ in range(3):
|
||||
size = int.from_bytes(key[:4], 'big')
|
||||
fields.append(key[4:4 + size])
|
||||
key = key[4 + size:]
|
||||
exponent, modulus = map(lambda value: int.from_bytes(value, 'big'), fields[1:])
|
||||
checks = {}
|
||||
for name in targets:
|
||||
data = Path(name).read_bytes()
|
||||
if name.endswith('.bin.signed'):
|
||||
payload, signature = data[:-128], data[-128:]
|
||||
expected = b'\x00\x01' + b'\xff' * 0x69 + b'\x00' + hashlib.sha1(payload[4:]).digest()
|
||||
assert pow(int.from_bytes(signature, 'big'), exponent, modulus).to_bytes(128, 'big') == expected
|
||||
assert int.from_bytes(payload[:4], 'little') == len(payload)
|
||||
assert int.from_bytes(payload[-12:-8], 'little') == zlib.crc32(payload[:-12])
|
||||
assert payload[-8:-4] == b'VERS'
|
||||
elf = name.removesuffix('.bin.signed') + '/main.elf'
|
||||
symbols = subprocess.check_output(['readelf', '-sW', elf], text=True)
|
||||
if variant == 'debug' and not name.endswith('/body_h7.bin.signed'):
|
||||
assert 'hyundai_canfd_ioniq6_bsm_wheel_valid' in symbols
|
||||
if '/bootstub.' in name:
|
||||
project = Path(name).name.removeprefix('bootstub.').removesuffix('.bin')
|
||||
symbols = subprocess.check_output(['readelf', '-sW', f'panda/board/obj/{project}/bootstub.elf'], text=True)
|
||||
assert ' RSA_verify' in symbols
|
||||
checks[name] = {'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data), 'mode': Path(name).stat().st_mode & 511}
|
||||
(OUT / 'all-firmware.json').write_text(json.dumps({'variant': variant, 'artifacts': checks}, indent=2))
|
||||
if variant == 'debug':
|
||||
import tarfile
|
||||
with tarfile.open(OUT / 'shipping-firmware.tar', 'w') as archive:
|
||||
for name in targets:
|
||||
archive.add(name, arcname=name)
|
||||
|
||||
def helper():
|
||||
import ctypes
|
||||
import struct
|
||||
target = Path('panda/tests/libpanda/libpanda.so')
|
||||
original = OUT / 'parent-libpanda.so'
|
||||
original.write_bytes(target.read_bytes())
|
||||
run('cc', '-fPIC', '-nostdlib', '-fno-builtin', '-std=gnu11', '-Wfatal-errors', '-Wno-pointer-to-int-cast',
|
||||
'-Ipanda/tests/libpanda', '-Ipanda', '-Ipanda/board', '-Iopendbc_repo', '-c',
|
||||
'panda/tests/libpanda/panda.c', '-o', str(OUT / 'panda.os'))
|
||||
run('cc', '-shared', str(OUT / 'panda.os'), '-o', str(target))
|
||||
for path in (original, target):
|
||||
assert struct.unpack_from('<H', path.read_bytes(), 18)[0] == 183
|
||||
ctypes.CDLL(str(path.resolve()))
|
||||
def exports(path):
|
||||
return {line.split()[-1] for line in subprocess.check_output(['nm', '-D', '--defined-only', str(path)], text=True).splitlines()}
|
||||
assert exports(original) == exports(target)
|
||||
deps = subprocess.check_output(['cc', '-MM', '-Ipanda/tests/libpanda', '-Ipanda', '-Ipanda/board', '-Iopendbc_repo',
|
||||
'panda/tests/libpanda/panda.c'], text=True)
|
||||
assert 'opendbc/safety/modes/hyundai_canfd.h' in deps
|
||||
(OUT / 'helper.json').write_text(json.dumps({'sha256': hashlib.sha256(target.read_bytes()).hexdigest(),
|
||||
'arch': 'AArch64', 'exportsEqual': True, 'dlopen': True, 'includesChangedHeader': True,
|
||||
'compiler': subprocess.check_output(['cc', '--version'], text=True), 'dependencies': deps}, indent=2))
|
||||
import tarfile
|
||||
with tarfile.open(OUT / 'shipping-helper.tar', 'w') as archive:
|
||||
archive.add(target, arcname=str(target))
|
||||
|
||||
if __name__ == '__main__':
|
||||
{'qualify': qualify, 'firmware': firmware, 'helper': helper}[sys.argv[1]]()
|
||||
@@ -0,0 +1,44 @@
|
||||
name: Ioniq 6 queued BSM qualification
|
||||
on:
|
||||
push:
|
||||
branches: [codex/ioniq6-bsm-queued-20261107]
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
host-native:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 35
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
SCALE: '1'
|
||||
RAYLIB_BACKEND: headless
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- run: git -c gc.auto=0 -c maintenance.auto=false fetch --no-tags --depth=1 --filter=blob:none origin 1d30dc343469e93967d2de3afdc966901009ed5d
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: '1.25.1'
|
||||
cache-dependency-path: mapd_repo/go.sum
|
||||
- working-directory: mapd_repo
|
||||
env:
|
||||
GOTOOLCHAIN: local
|
||||
run: go mod download
|
||||
- run: ./tools/op.sh setup
|
||||
- name: Build matching Linux dependencies
|
||||
run: >-
|
||||
.venv/bin/scons -j2
|
||||
openpilot/starpilot/ui/native/_raylib_extra.so
|
||||
openpilot/common/libparams_c.so
|
||||
msgq_repo/msgq/_ipc.so
|
||||
msgq_repo/msgq/visionipc/_visionipc.so
|
||||
opendbc_repo/opendbc/can/_raw_value.so
|
||||
opendbc_repo/opendbc/dbc
|
||||
openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/c_generated_code/acados_ocp_solver_pyx.so
|
||||
openpilot/selfdrive/locationd/models/generated/libcar.so
|
||||
- run: .venv/bin/python .github/bsm-qualification.py qualify
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: bsm-host-native
|
||||
path: bsm-results/
|
||||
if-no-files-found: error
|
||||
@@ -597,8 +597,10 @@ class CarController(CarControllerBase):
|
||||
can_now_ns = getattr(CS, "ioniq6_bsm_now_ns", 0)
|
||||
status = (sources.status(can_now_ns) if sources is not None and can_now_ns > self.ioniq6_bsm_last_can_ns else None)
|
||||
if status is not None:
|
||||
# Neutralize only the injected dashboard indication below 5 mph.
|
||||
if CS.out.vEgoRaw < 5 * CV.MPH_TO_MS:
|
||||
left_queued = CC.leftBlinker and not CC.rightBlinker
|
||||
right_queued = CC.rightBlinker and not CC.leftBlinker
|
||||
if (CS.out.vEgoRaw < 5 * CV.MPH_TO_MS or
|
||||
status.left == 2 and not left_queued or status.right == 2 and not right_queued):
|
||||
status = BlindspotStatus(0, 0)
|
||||
can_sends.extend(hyundaicanfd.create_ioniq6_blindspot_status(self.ioniq6_bsm_counter, status))
|
||||
self.ioniq6_bsm_counter = (self.ioniq6_bsm_counter + 1) & 0xff
|
||||
|
||||
@@ -168,10 +168,12 @@ class TestIoniq6BlindspotStatus(unittest.TestCase):
|
||||
lamp_source_ns=t, left_lamp=left_lamp, right_lamp=right_lamp)
|
||||
ci.CS.ioniq6_bsm_sources = sources
|
||||
source_status = sources.status(t)
|
||||
assert source_status is not None
|
||||
counter = controller.ioniq6_bsm_counter
|
||||
_, frames = controller.update(control.as_reader(), ci.CS, t)
|
||||
status_frames = [(addr, payload, bus) for addr, payload, bus in frames if addr in (0x1BA, 0x1E5)]
|
||||
expected = BlindspotStatus(0, 0) if speed < threshold else source_status
|
||||
expected = (BlindspotStatus(0, 0) if speed < threshold or 2 in (source_status.left, source_status.right)
|
||||
else source_status)
|
||||
self.assertEqual(status_frames, create_ioniq6_blindspot_status(counter, expected))
|
||||
self.assertEqual(sources.status(t), source_status)
|
||||
for addr, payload, _ in status_frames:
|
||||
@@ -182,3 +184,89 @@ class TestIoniq6BlindspotStatus(unittest.TestCase):
|
||||
_, skipped = controller.update(control.as_reader(), ci.CS, t + i * 10_000_000)
|
||||
self.assertFalse(any(addr in (0x1BA, 0x1E5) for addr, _, _ in skipped))
|
||||
ci.CS.out = out
|
||||
|
||||
def test_controller_requires_a_matching_queued_lane_change_for_escalation(self):
|
||||
cp = long_params()
|
||||
ci = CarInterface(cp)
|
||||
controller = CarController(DBC[cp.carFingerprint], cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
control = structs.CarControl()
|
||||
controller.ioniq6_bsm_counter = 250
|
||||
for physical_left in (True, False):
|
||||
for request in ('matching', 'none', 'opposite', 'hazards', 'matching', 'none'):
|
||||
with self.subTest(left=physical_left, request=request):
|
||||
t = 10_000_000_000 + controller.frame * 10_000_000
|
||||
ci.update([(t, [packer.make_can_msg('BLINDSPOTS_FRONT_CORNER_2', 1,
|
||||
{'SIDE_DETECT_STATE': 0x10 if physical_left else 0x08}),
|
||||
packer.make_can_msg('BLINKERS', 1, {})])])
|
||||
ci.CS.out.vEgoRaw = 10.0
|
||||
sources = Ioniq6BlindspotSources()
|
||||
sources.observe(corner_source_ns=t, corner_state=0x10 if physical_left else 0x08,
|
||||
lamp_source_ns=t, left_lamp=physical_left, right_lamp=not physical_left)
|
||||
ci.CS.ioniq6_bsm_sources = sources
|
||||
control.leftBlinker = request == 'hazards' or request == ('matching' if physical_left else 'opposite')
|
||||
control.rightBlinker = request == 'hazards' or request == ('opposite' if physical_left else 'matching')
|
||||
counter = controller.ioniq6_bsm_counter
|
||||
_, frames = controller.update(control.as_reader(), ci.CS, t)
|
||||
actual = [(addr, payload, bus) for addr, payload, bus in frames if addr in (0x1BA, 0x1E5)]
|
||||
source_status = sources.status(t)
|
||||
assert source_status is not None
|
||||
expected = source_status if request == 'matching' else BlindspotStatus(0, 0)
|
||||
self.assertEqual(actual, create_ioniq6_blindspot_status(counter, expected), 'UNQUEUED_BLINDSPOT_WARNING')
|
||||
self.assertEqual(sources.status(t), BlindspotStatus(2, 0) if physical_left else BlindspotStatus(0, 2))
|
||||
for offset in range(1, 5):
|
||||
_, skipped = controller.update(control.as_reader(), ci.CS, t + offset * 10_000_000)
|
||||
self.assertFalse(any(addr in (0x1BA, 0x1E5) for addr, _, _ in skipped))
|
||||
|
||||
def test_actual_lane_change_request_completion_and_turn_with_held_stalk(self):
|
||||
from openpilot.cereal import log
|
||||
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||
from openpilot.starpilot.lateral.lane_change_preferences import LaneChangePolicy
|
||||
|
||||
for left in (True, False):
|
||||
cp = long_params()
|
||||
ci = CarInterface(cp)
|
||||
controller = CarController(DBC[cp.carFingerprint], cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
state = structs.CarState(vEgo=20.0, leftBlinker=left, rightBlinker=not left)
|
||||
helper = DesireHelper(LaneChangePolicy(auto_lane_change=True))
|
||||
|
||||
def check(expected, helper, *, controller=controller, ci=ci, packer=packer, left=left):
|
||||
control = structs.CarControl()
|
||||
t = 10_000_000_000 + controller.frame * 10_000_000
|
||||
ci.update([(t, [packer.make_can_msg('BLINDSPOTS_FRONT_CORNER_2', 1,
|
||||
{'SIDE_DETECT_STATE': 0x10 if left else 0x08}),
|
||||
packer.make_can_msg('BLINKERS', 1, {'LEFT_LAMP': int(left), 'RIGHT_LAMP': int(not left)})])])
|
||||
ci.CS.out.vEgoRaw = 20.0
|
||||
queued = helper.lane_change_state != log.LaneChangeState.off
|
||||
control.leftBlinker = queued and helper.lane_change_direction == log.LaneChangeDirection.left
|
||||
control.rightBlinker = queued and helper.lane_change_direction == log.LaneChangeDirection.right
|
||||
counter = controller.ioniq6_bsm_counter
|
||||
_, frames = controller.update(control.as_reader(), ci.CS, t)
|
||||
self.assertEqual([frame for frame in frames if frame[0] in (0x1BA, 0x1E5)],
|
||||
create_ioniq6_blindspot_status(counter, expected))
|
||||
for offset in range(1, 5):
|
||||
controller.update(control.as_reader(), ci.CS, t + offset * 10_000_000)
|
||||
|
||||
# Missing automatic-lane evidence does not cancel a manual pending request.
|
||||
helper.update(state, True, 1.0, auto_evidence=False, engaged=True)
|
||||
helper.update(state, True, 1.0, auto_evidence=False, engaged=True)
|
||||
self.assertEqual(helper.auto_status, 'laneUnavailable')
|
||||
self.assertEqual(helper.lane_change_state, log.LaneChangeState.preLaneChange)
|
||||
check(BlindspotStatus(2, 0) if left else BlindspotStatus(0, 2), helper)
|
||||
state.steeringPressed = True
|
||||
state.steeringTorque = 1.0 if left else -1.0
|
||||
helper.update(state, True, 1.0, engaged=True)
|
||||
self.assertEqual(helper.lane_change_state, log.LaneChangeState.laneChangeStarting)
|
||||
check(BlindspotStatus(2, 0) if left else BlindspotStatus(0, 2), helper)
|
||||
state.steeringPressed = False
|
||||
for _ in range(12):
|
||||
helper.update(state, True, 0.0, engaged=True)
|
||||
self.assertEqual(helper.lane_change_state, log.LaneChangeState.off)
|
||||
check(BlindspotStatus(0, 0), helper)
|
||||
helper.update(state, True, 0.0, engaged=True)
|
||||
check(BlindspotStatus(0, 0), helper)
|
||||
helper = DesireHelper()
|
||||
helper.update(state, True, 1.0, navigation_turn=True)
|
||||
self.assertEqual(helper.lane_change_state, log.LaneChangeState.off)
|
||||
check(BlindspotStatus(0, 0), helper)
|
||||
|
||||
@@ -225,7 +225,7 @@ static bool hyundai_canfd_gv60_heartbeat_seen = false;
|
||||
static uint8_t hyundai_canfd_gv60_heartbeat_counter = 0U;
|
||||
static bool hyundai_canfd_ioniq6_heartbeat_seen = false;
|
||||
static uint8_t hyundai_canfd_ioniq6_heartbeat_counter = 0U;
|
||||
static bool hyundai_canfd_ioniq6_bsm_low_speed = false;
|
||||
static bool hyundai_canfd_ioniq6_bsm_wheel_valid = false;
|
||||
static bool hyundai_canfd_ioniq6_bsm_wheel_seen = false;
|
||||
static uint8_t hyundai_canfd_ioniq6_bsm_wheel_counter = 0U;
|
||||
static uint32_t hyundai_canfd_ioniq6_bsm_wheel_ts = 0U;
|
||||
@@ -763,11 +763,10 @@ static void hyundai_canfd_rx_hook(const CANPacket_t *msg) {
|
||||
// average of all 4 wheel speeds. Conversion: raw * 0.03125 / 3.6 = m/s
|
||||
UPDATE_VEHICLE_SPEED((fr + rr + rl + fl) / 4.0 * 0.03125 * KPH_TO_MS);
|
||||
if (hyundai_canfd_ioniq6_long) {
|
||||
// Mean wheel speed < 5 mph: raw sum < 2.2352 * 4 * 3.6 / 0.03125.
|
||||
// This hook runs only for valid required RX; TX also checks its freshness.
|
||||
const bool fresh_counter = !hyundai_canfd_ioniq6_bsm_wheel_seen ||
|
||||
(msg->data[2] == (uint8_t)(hyundai_canfd_ioniq6_bsm_wheel_counter + 1U));
|
||||
hyundai_canfd_ioniq6_bsm_low_speed = fresh_counter && ((fr + rr + rl + fl) < 1030U);
|
||||
hyundai_canfd_ioniq6_bsm_wheel_valid = fresh_counter;
|
||||
if (fresh_counter) {
|
||||
hyundai_canfd_ioniq6_bsm_wheel_ts = microsecond_timer_get();
|
||||
}
|
||||
@@ -1037,7 +1036,7 @@ static bool hyundai_canfd_tx_hook_valid(const CANPacket_t *msg) {
|
||||
body[7] = 0x80U | (SAFETY_MAX(left, right) << 3U);
|
||||
body[16] = left | (right << 2U);
|
||||
bool physical_body = true;
|
||||
bool neutral_body = hyundai_canfd_ioniq6_bsm_low_speed &&
|
||||
bool neutral_body = hyundai_canfd_ioniq6_bsm_wheel_valid &&
|
||||
(safety_get_ts_elapsed(now, hyundai_canfd_ioniq6_bsm_wheel_ts) <= 100000U);
|
||||
for (int i = 3; i < 24; i++) {
|
||||
physical_body &= msg->data[i] == body[i];
|
||||
@@ -1314,7 +1313,7 @@ static void hyundai_canfd_reset_ioniq6_state(void) {
|
||||
hyundai_canfd_gv60_heartbeat_counter = 0U;
|
||||
hyundai_canfd_ioniq6_heartbeat_seen = false;
|
||||
hyundai_canfd_ioniq6_heartbeat_counter = 0U;
|
||||
hyundai_canfd_ioniq6_bsm_low_speed = false;
|
||||
hyundai_canfd_ioniq6_bsm_wheel_valid = false;
|
||||
hyundai_canfd_ioniq6_bsm_wheel_seen = false;
|
||||
hyundai_canfd_ioniq6_bsm_wheel_counter = 0U;
|
||||
hyundai_canfd_ioniq6_bsm_wheel_ts = 0U;
|
||||
|
||||
@@ -351,11 +351,11 @@ class TestHyundaiIoniq6Long(unittest.TestCase):
|
||||
("WHL_SpdFLVal", "WHL_SpdFRVal", "WHL_SpdRLVal", "WHL_SpdRRVal"), raw_speeds, strict=True)}
|
||||
return packer.make_can_msg("WHEEL_SPEEDS", 1, {**values, "COUNTER": counter})
|
||||
|
||||
def test_bsm_neutral_only_below_physical_speed_boundary(self):
|
||||
def test_bsm_neutral_advisory_requires_fresh_wheels_at_all_speeds(self):
|
||||
if self.release:
|
||||
self.skipTest("RELEASE denies the Ioniq 6 LONG profiles")
|
||||
# 5 mph is between representable sums 1029 and 1030; test unequal wheels too.
|
||||
samples = ((0, 0, 0, 0), (257, 257, 257, 258), (257, 257, 258, 258), (258, 258, 258, 258))
|
||||
# Keep the old speed boundary and highway speeds in the advisory matrix.
|
||||
samples = ((0, 0, 0, 0), (257, 257, 257, 258), (257, 257, 258, 258), (258, 258, 258, 258), (4000, 4000, 4000, 4000))
|
||||
for raw in (*RAW_LONG.values(), *RAW_AOL.values()):
|
||||
_, cp = params("lkas_alt" if raw & 0x80 else "lkas")
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
@@ -365,9 +365,8 @@ class TestHyundaiIoniq6Long(unittest.TestCase):
|
||||
with self.subTest(profile=hex(raw), speeds=speeds):
|
||||
self.assertTrue(self.safety.safety_rx_hook(self.packet(self.bsm_wheel_frame(packer, speeds, counter))))
|
||||
rear, front = create_ioniq6_blindspot_status(counter, BlindspotStatus(0, 0))
|
||||
permitted = sum(speeds) < 1030
|
||||
self.assertEqual(self.safety.safety_tx_hook(self.packet(rear)), permitted)
|
||||
self.assertEqual(self.safety.safety_tx_hook(self.packet(front)), permitted)
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.packet(rear)))
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.packet(front)))
|
||||
# Original physical indications remain valid, independently of speed.
|
||||
for frame in create_ioniq6_blindspot_status(counter + 30, BlindspotStatus(2, 2)):
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.packet(frame)))
|
||||
@@ -429,10 +428,10 @@ class TestHyundaiIoniq6Long(unittest.TestCase):
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.packet(rear)))
|
||||
self.safety.set_timer(1_200_002)
|
||||
self.assertFalse(self.safety.safety_tx_hook(self.packet(front)))
|
||||
# A low-speed packet on the wrong route or with the wrong length cannot
|
||||
# replace a valid above-threshold physical wheel observation.
|
||||
# Wrong-route and short packets cannot refresh expired wheel evidence.
|
||||
other_sources(5, 1_210_000)
|
||||
self.assertTrue(self.safety.safety_rx_hook(self.packet(self.bsm_wheel_frame(packer, (300, 300, 300, 300), 5))))
|
||||
other_sources(6, 1_310_001)
|
||||
low = self.bsm_wheel_frame(packer, (0, 0, 0, 0), 6)
|
||||
for invalid in ((low[0], low[1], 0), (low[0], low[1][:8], 1)):
|
||||
self.safety.safety_rx_hook(self.packet(invalid))
|
||||
@@ -478,36 +477,38 @@ class TestHyundaiIoniq6Long(unittest.TestCase):
|
||||
for topology in ("lkas", "lkas_alt"):
|
||||
for aol in (False, True):
|
||||
for raw_speeds in ((0, 0, 0, 0), (257, 257, 257, 258), (257, 257, 258, 258), (300, 300, 300, 300)):
|
||||
with self.subTest(topology=topology, aol=aol, speeds=raw_speeds):
|
||||
_, cp = params(topology)
|
||||
raw = (RAW_AOL if aol else RAW_LONG)[topology]
|
||||
cp.safetyConfigs[-1].safetyParam = raw
|
||||
ci = CarInterface(cp)
|
||||
controller = CarController(DBC[cp.carFingerprint], cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
bus = CanBus(cp)
|
||||
inputs = [packer.make_can_msg(name, bus.ECAN, values) for name, values in (
|
||||
("ACCELERATOR", {"GEAR": 5, "COUNTER": 1}), ("TCS", {"COUNTER": 1}),
|
||||
("WHEEL_SPEEDS", {"COUNTER": 1}), ("MDPS", {"COUNTER": 1}),
|
||||
("CRUISE_BUTTONS", {"COUNTER": 1}),
|
||||
("BLINDSPOTS_FRONT_CORNER_2", {"COUNTER": 1, "SIDE_DETECT_STATE": 0x18}),
|
||||
("BLINKERS", {"LEFT_LAMP": 1}),
|
||||
)]
|
||||
inputs = [frame for frame in inputs if frame[0] != 0xA0]
|
||||
inputs.append(self.bsm_wheel_frame(packer, raw_speeds, 1))
|
||||
inputs.append(packer.make_can_msg("CAM_0x362" if topology == "lkas_alt" else "CAM_0x2a4", bus.CAM, {}))
|
||||
ci.update([(1_000_000_000, inputs)])
|
||||
self.mode(raw)
|
||||
for frame in inputs:
|
||||
if frame[2] == bus.ECAN:
|
||||
self.assertTrue(self.safety.safety_rx_hook(self.packet(frame)), hex(frame[0]))
|
||||
control = structs.CarControl()
|
||||
_, sent = controller.update(control.as_reader(), ci.CS, 1_000_000_000)
|
||||
pair = [frame for frame in sent if frame[0] in (0x1BA, 0x1E5)]
|
||||
expected = BlindspotStatus(0, 0) if sum(raw_speeds) < 1030 else BlindspotStatus(2, 1)
|
||||
self.assertEqual(pair, create_ioniq6_blindspot_status(0, expected))
|
||||
for frame in pair:
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.packet(frame)), (topology, aol, hex(frame[0])))
|
||||
for queued in (False, True):
|
||||
with self.subTest(topology=topology, aol=aol, speeds=raw_speeds, queued=queued):
|
||||
_, cp = params(topology)
|
||||
raw = (RAW_AOL if aol else RAW_LONG)[topology]
|
||||
cp.safetyConfigs[-1].safetyParam = raw
|
||||
ci = CarInterface(cp)
|
||||
controller = CarController(DBC[cp.carFingerprint], cp)
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
bus = CanBus(cp)
|
||||
inputs = [packer.make_can_msg(name, bus.ECAN, values) for name, values in (
|
||||
("ACCELERATOR", {"GEAR": 5, "COUNTER": 1}), ("TCS", {"COUNTER": 1}),
|
||||
("WHEEL_SPEEDS", {"COUNTER": 1}), ("MDPS", {"COUNTER": 1}),
|
||||
("CRUISE_BUTTONS", {"COUNTER": 1}),
|
||||
("BLINDSPOTS_FRONT_CORNER_2", {"COUNTER": 1, "SIDE_DETECT_STATE": 0x18}),
|
||||
("BLINKERS", {"LEFT_LAMP": 1}),
|
||||
)]
|
||||
inputs = [frame for frame in inputs if frame[0] != 0xA0]
|
||||
inputs.append(self.bsm_wheel_frame(packer, raw_speeds, 1))
|
||||
inputs.append(packer.make_can_msg("CAM_0x362" if topology == "lkas_alt" else "CAM_0x2a4", bus.CAM, {}))
|
||||
ci.update([(1_000_000_000, inputs)])
|
||||
self.mode(raw)
|
||||
for frame in inputs:
|
||||
if frame[2] == bus.ECAN:
|
||||
self.assertTrue(self.safety.safety_rx_hook(self.packet(frame)), hex(frame[0]))
|
||||
control = structs.CarControl()
|
||||
control.leftBlinker = queued
|
||||
_, sent = controller.update(control.as_reader(), ci.CS, 1_000_000_000)
|
||||
pair = [frame for frame in sent if frame[0] in (0x1BA, 0x1E5)]
|
||||
expected = BlindspotStatus(0, 0) if not queued or sum(raw_speeds) < 1030 else BlindspotStatus(2, 1)
|
||||
self.assertEqual(pair, create_ioniq6_blindspot_status(0, expected))
|
||||
for frame in pair:
|
||||
self.assertTrue(self.safety.safety_tx_hook(self.packet(frame)), (topology, aol, hex(frame[0])))
|
||||
|
||||
def test_exhaustive_angle_bit_namespace_and_release_denial(self):
|
||||
for low in range(0x8000):
|
||||
|
||||
Reference in New Issue
Block a user