mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-01 11:53:46 +08:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a12dbd0ad | |||
| 5bc666676a | |||
| 2a528414ed | |||
| ecda0c61d9 | |||
| 399a40ca22 | |||
| e47133be1a | |||
| 5ce64a49a8 | |||
| 4c47955498 | |||
| fab2494f8e | |||
| 96a75ba908 | |||
| 3a41fe663a | |||
| 678af78347 | |||
| 9d8a523471 | |||
| b7cd0caff2 | |||
| cdc6b3bd68 | |||
| 7f0c5673b4 | |||
| 2a13cc7fe2 | |||
| 7c6038fe28 | |||
| 5925aecd5b | |||
| e6390c32e1 | |||
| 1590a5cc2b | |||
| 78d412d1d0 | |||
| d34a756929 |
@@ -0,0 +1,56 @@
|
||||
name: Fleet controller safety
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'opendbc_repo/**'
|
||||
- 'selfdrive/car/**'
|
||||
- 'starpilot/car/**'
|
||||
- 'starpilot/controls/**'
|
||||
- 'cereal/**'
|
||||
- '.github/workflows/fleet_safety.yaml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
harness:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- run: python -m pip install -r selfdrive/car/tests/fleet_requirements.txt
|
||||
- name: Audit accounting and runner failures
|
||||
run: >-
|
||||
python -m pytest --noconftest -o addopts='' -q
|
||||
selfdrive/car/tests/test_fleet_safety_core.py
|
||||
selfdrive/car/tests/test_fleet_safety_runner.py
|
||||
opendbc_repo/opendbc/safety/tests/safety_replay/test_replay_drive.py
|
||||
recorded_fleet:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [0, 1, 2, 3, 4, 5, 6, 7]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- run: python -m pip install -r selfdrive/car/tests/fleet_requirements.txt
|
||||
- name: Current controllers against freshly compiled release safety
|
||||
run: >-
|
||||
python -m selfdrive.car.tests.fleet_safety --all --release
|
||||
--shard-count 8 --shard-index ${{ matrix.shard }}
|
||||
--out selfdrive/car/tests/fleet_results/ci
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: fleet-safety-${{ matrix.shard }}
|
||||
path: |
|
||||
selfdrive/car/tests/fleet_results/ci/**/*.json
|
||||
selfdrive/car/tests/fleet_results/ci/**/*.log
|
||||
Binary file not shown.
@@ -198,7 +198,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"AggressiveJerkSpeedDecrease", {PERSISTENT, FLOAT, "50.0", "50.0", 3}},
|
||||
{"AlertVolumeControl", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"AllowImpossibleAcceleration", {PERSISTENT, BOOL, "0", "0", 3}},
|
||||
{"AlwaysOnLateral", {PERSISTENT, BOOL, "1", "0", 0, SETTINGS_SIMPLE}},
|
||||
{"AlwaysOnLateral", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
|
||||
{"AlwaysOnLateralLKAS", {PERSISTENT, BOOL, "1", "0", 2}},
|
||||
{"ApiCache_DriveStats", {PERSISTENT, JSON, "{}", "{}"}},
|
||||
{"AutomaticallyDownloadModels", {PERSISTENT, BOOL, "1", "0", 1}},
|
||||
@@ -702,6 +702,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"StandbyMode", {PERSISTENT, BOOL, "0", "0", 1, SETTINGS_SIMPLE}},
|
||||
{"StandbyWakeButton", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"StandbyButtonPressTime", {CLEAR_ON_MANAGER_START | DONT_LOG, INT, "0", "0"}},
|
||||
{"ScreenOffToggleCounter", {CLEAR_ON_MANAGER_START | DONT_LOG, INT, "0", "0"}},
|
||||
{"StandbyWakeEngage", {PERSISTENT, BOOL, "1", "1", 2, SETTINGS_SIMPLE}},
|
||||
{"StandbyWakeDisengage", {PERSISTENT, BOOL, "1", "1", 2, SETTINGS_SIMPLE}},
|
||||
{"StandbyWakeInfoAlert", {PERSISTENT, BOOL, "1", "1", 2, SETTINGS_SIMPLE}},
|
||||
@@ -740,6 +741,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"SubaruSNG", {PERSISTENT, BOOL, "1", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"SubaruSNGManualParkingBrake", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"SubaruStopStartOff", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"SubaruAvhStartup", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"SubaruRedneckCruise", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"TacoTune", {PERSISTENT, BOOL, "0", "0", 2}},
|
||||
{"TeslaCoopSteering", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,86 @@
|
||||
# Fleet offline audit — September 21, 2026
|
||||
|
||||
This is a first-pass fault and coverage inventory, not fleet driving clearance.
|
||||
No hardware was accessed and no controller or safety policy was changed by this
|
||||
audit. Other work was concurrently modifying the checkout; per-case source and
|
||||
library hashes identify the tested implementations.
|
||||
|
||||
The full debug-library run visited all 345 platforms. It evaluated both recorded
|
||||
and AOL-main scenarios for 287 registered routes, plus 108 missing-route entries:
|
||||
|
||||
| Result | Cases |
|
||||
|---|---:|
|
||||
| Pass within the stated scope | 103 |
|
||||
| Failed checks, requiring triage | 123 |
|
||||
| Missing coverage | 288 |
|
||||
| Could not evaluate | 168 |
|
||||
| Total | 682 |
|
||||
|
||||
These are case counts, not numbers of unsafe cars. Missing coverage includes all
|
||||
108 platforms without routes and segments without active transitions. Evaluation
|
||||
errors include unavailable recordings and identities that do not match the
|
||||
registered platform after the existing fingerprint migration. Old logs must be
|
||||
normalized explicitly, not quietly substituted for another car.
|
||||
|
||||
Full local evidence is under
|
||||
`selfdrive/car/tests/fleet_results/full_fleet/results.json`, with per-shard build,
|
||||
case and worker logs. Generated evidence is ignored by Git.
|
||||
|
||||
The follow-up full release-library run also completed 682 cases: **102 pass,
|
||||
153 failed, 289 uncovered, 138 evaluation errors**. Evidence is under
|
||||
`selfdrive/car/tests/fleet_results/release_fleet_checked/results.json`.
|
||||
The two batches had different download availability and ran against a changing
|
||||
working tree; their count difference is not an isolated debug-versus-release
|
||||
experiment. Both correctly exit nonzero. The release batch is not fleet clearance.
|
||||
|
||||
## Confirmed distinctions from failure triage
|
||||
|
||||
**Hyundai Custin — controller/safety capability mismatch.** The registered
|
||||
segment `0bbe367c98fa1538/2023-09-16--00-16-49/2` contains 600 camera-bus
|
||||
messages at 0x53e, all eight bytes, none six. `CarInterfaceBase.get_starpilot_params`
|
||||
in `opendbc_repo/opendbc/car/interfaces.py` enables HAS_LKAS12 by address alone.
|
||||
Hyundai `CarState.update` and `CarController.update` then produce six-byte LKAS12
|
||||
replacements. In `opendbc_repo/opendbc/safety/modes/hyundai.h`,
|
||||
`hyundai_rx_all_hook` only enables replacement after receiving a six-byte camera
|
||||
message, and `hyundai_tx_hook` correctly rejects the unsolicited replacement.
|
||||
Both scenarios reject 5,799 such packets. A fresh-library probe also reproduced
|
||||
the six-byte/eight-byte distinction. Repair requires a controller capability and
|
||||
parser regression test; do not broaden the safety allowlist to hide the mismatch.
|
||||
|
||||
**Honda Civic Bosch — incompatible historical control requests.** All 5,997
|
||||
recorded requests in the inspected 2020 fixture have enabled/latActive/longActive
|
||||
false but resume true; all 6,000 cruise-state CAN messages are disabled. Current
|
||||
controller output is RES_ACCEL at 0x296, which current safety correctly blocks.
|
||||
The AOL probe suppresses resume and passes. Investigate historical command/schema
|
||||
semantics before calling this a current steering defect.
|
||||
|
||||
**Ford Escape — mid-segment initialization artifact.** The first cruise-enabled
|
||||
0x165 enables controls, but the immediately following 0x202 reaches
|
||||
`speed_mismatch_check` before safety has nonzero speed history. Controls are
|
||||
revoked; cruise stays enabled for the entire segment so `pcm_cruise_check` sees
|
||||
no new rising edge. Relay health remains good. This reproduces with both recorded
|
||||
and default alternative experience. A two-second scoring warmup does not repair
|
||||
the latch. This needs recorded preroll/initialization coverage, not force-setting
|
||||
`controls_allowed` or changing vehicle safety.
|
||||
|
||||
## Tesla and AOL scope
|
||||
|
||||
In the full debug-library run, the Model 3 route and the second Model Y route
|
||||
passed both scenarios. The first Model Y route lacked a lateral transition;
|
||||
its AOL case also lacked requested steering under AOL-only safety permission.
|
||||
Model X was uncovered as a current dashcam-only configuration. The Model S HW1
|
||||
and Pre-AP entries have no registered routes. None of these findings reproduces
|
||||
or disproves the exact hackathon oscillation without its trace.
|
||||
|
||||
The separate actual StarPilotCard synthetic-input suite passed 964 checks with
|
||||
72 explicit active-sequence gaps. All 345 disabled configurations were checked;
|
||||
309 platforms completed both active modes. These tests check state-machine gates
|
||||
and stable sequences, not the entire selfdrived-to-Panda feedback loop.
|
||||
|
||||
The test-harness regressions pass 34 tests. They cover pre-hook AOL authorization,
|
||||
strict configuration/bus routing, rejected active packets, expected negative
|
||||
checks, empty activity, worker crashes, stale reports and build failures.
|
||||
|
||||
See `FLEET_SAFETY_TESTING.md` for commands, CI scope and limitations. The workflow
|
||||
has been added locally but not published or run on GitHub, and branch protection
|
||||
has not been changed. The full fleet is not green.
|
||||
@@ -0,0 +1,112 @@
|
||||
# Offline fleet controller and safety checks
|
||||
|
||||
The runner in `selfdrive/car/tests/fleet_safety.py` enumerates every platform in
|
||||
the current checkout and uses `opendbc.car.tests.routes`. It runs current
|
||||
CarInterface/CarState/CarController code against recorded CAN and actuator
|
||||
requests, then checks each newly generated CAN packet with freshly compiled
|
||||
current safety hooks. It never connects to a Panda or starts vehicle processes.
|
||||
|
||||
Run from the repository root with the repository Python environment:
|
||||
|
||||
```sh
|
||||
python -m selfdrive.car.tests.fleet_safety --inventory
|
||||
python -m selfdrive.car.tests.fleet_safety --platform TESLA_MODEL_Y --release
|
||||
python -m selfdrive.car.tests.fleet_safety --all --release
|
||||
```
|
||||
|
||||
An isolated dependency set is in `selfdrive/car/tests/fleet_requirements.txt`.
|
||||
The runner needs a C compiler. It does not use a previously staged libsafety.
|
||||
Without `--release`, the safety library enables ALLOW_DEBUG, like the existing
|
||||
safety unit tests. Release checks are needed as well: a debug-only hook must not
|
||||
be mistaken for an available production configuration. Release host builds retain
|
||||
unused-variable warnings without treating that specific diagnostic as an error.
|
||||
|
||||
For parallel runs, use separate output directories:
|
||||
|
||||
```sh
|
||||
python -m selfdrive.car.tests.fleet_safety --all --release \
|
||||
--shard-count 8 --shard-index 0 --out selfdrive/car/tests/fleet_results/shard_0
|
||||
```
|
||||
|
||||
Run indices 0 through 7. Each has its own library copies, worker processes,
|
||||
parameter namespace, logs and results. `--local-log` allows a local rlog for one
|
||||
explicitly selected platform. Route IDs and old fingerprint aliases must match;
|
||||
the harness does not silently treat another vehicle's log as that platform.
|
||||
|
||||
## What is checked
|
||||
|
||||
- Recorded commands under the current default feature configuration.
|
||||
- A separate AOL MAIN-availability controller/safety probe, using recorded
|
||||
actuator values with longitudinal requests and cruise button requests off.
|
||||
- Actual per-Panda safety model, CP/FPCP safety-param OR, alternative-experience
|
||||
OR, and strict four-bus routing, matching production configuration assembly.
|
||||
- Incoming CAN, current CarState validity and safety receive health.
|
||||
- Every emitted TX, including inactive-state packets; unexpected rejection fails.
|
||||
- Pre-hook normal/AOL/longitudinal permissions, so a rejection that revokes
|
||||
authorization cannot disappear from the failure accounting.
|
||||
- Active requests, accepted active TX, engagement transitions, and AOL-only
|
||||
safety authorization coverage. Sparse commands and absent transitions cannot
|
||||
qualify as complete coverage.
|
||||
|
||||
There is a two-second unscored fixture startup interval. Controller and safety
|
||||
history still receive messages during it. The harness does not force safety
|
||||
authorization or clear a relay fault to manufacture a passing result.
|
||||
|
||||
## Results are deliberately strict
|
||||
|
||||
`pass` means the case satisfied these specific checks and coverage requirements.
|
||||
`failed` means a hook/health check failed and needs investigation. `uncovered`
|
||||
means the scenario was not demonstrated, including missing routes, dashcam-only
|
||||
interfaces and segments without transitions. `error` means the case could not be
|
||||
evaluated, such as download failure or mismatched fixture identity. Anything
|
||||
other than pass makes the command exit nonzero. Existing `non_tested_cars`
|
||||
exemptions remain visible coverage gaps.
|
||||
|
||||
Reports include frame counters, bounded rejected packet evidence, source hashes,
|
||||
effective safety configurations and build provenance. Worker results carry a
|
||||
unique execution ID; a crash, stale result or inconsistent exit code cannot be
|
||||
reused as a pass. JSON and logs live under the ignored `fleet_results` directory.
|
||||
|
||||
The AOL probe is **not** a complete simulation of StarPilotCard, selfdrived,
|
||||
controls mismatch handling or a vehicle ECU. Recorded commands may also reflect
|
||||
historical settings different from current defaults. A blocked historical resume
|
||||
request is not automatically a steering bug. Investigate each failure before
|
||||
changing code. Do not widen safety permissions to make tests green.
|
||||
|
||||
## Continuous integration and remaining coverage
|
||||
|
||||
`starpilot/controls/tests/test_fleet_aol.py` separately exercises the actual
|
||||
StarPilotCard state machine with isolated synthetic inputs for every platform.
|
||||
It tests feature-off behavior, steady engagement, AOL-only operation, brake
|
||||
pause, native/StarPilot immediate-disable alerts, calibration and gear gates.
|
||||
It uses empty firmware/fingerprint fixtures, so optional vehicle configurations
|
||||
are not covered by these sequences. Run it with a compatible built host runtime:
|
||||
|
||||
```sh
|
||||
python -m pytest --noconftest -o addopts='' -q starpilot/controls/tests/test_fleet_aol.py
|
||||
```
|
||||
|
||||
The first run passed 964 checks and explicitly skipped 72 active sequences:
|
||||
309 platforms exercised both active modes; 36 platforms had two gaps each
|
||||
(30 dashcam-only, one notCar, four Volvo policy exclusions, one Pre-AP external
|
||||
authorization dependency). All 345 feature-off checks passed. A separate
|
||||
Pre-AP authorization input boundary test is synthetic, not proof of actual
|
||||
Panda authorization. These tests were run against the current working tree,
|
||||
including concurrent Pre-AP changes; they do not certify an earlier commit.
|
||||
The lightweight CI workflow below does not build the native runtime required
|
||||
by this separate state-machine suite.
|
||||
|
||||
`.github/workflows/fleet_safety.yaml` adds harness tests and eight release-mode
|
||||
recorded-route shards on relevant pull requests and manual runs. Missing coverage
|
||||
is not converted to a skip or allowed failure. The current fleet is not green;
|
||||
this workflow will expose that fact. It has not been executed on GitHub from this
|
||||
local task. Requiring it for merge also needs repository branch protection; a
|
||||
workflow file alone does not change repository settings.
|
||||
|
||||
As of the first September 21 inventory there are 345 platforms, 287 registered
|
||||
routes across 237 platforms, and 108 platforms with no registered route. A route
|
||||
entry does not guarantee valid, downloadable logs or all necessary maneuvers.
|
||||
Every optional harness, longitudinal mode, safety parameter, firmware generation
|
||||
and AOL configuration still needs explicit coverage. Offline checks reduce
|
||||
blind spots; they do not certify every physical vehicle or reproduce an incident
|
||||
whose CAN trace was not retained.
|
||||
@@ -811,7 +811,7 @@ class CarController(CarControllerBase):
|
||||
if not self.long_active_ecu:
|
||||
if self.cancel_counter > CANCEL_BUTTON_DELAY_FRAMES:
|
||||
can_sends.append(hyundaican.create_clu11(self.packer, self.frame, CS.clu11, Buttons.CANCEL, self.CP))
|
||||
elif self._ray_pedal and CC.longActive and CS.out.cruiseState.enabled:
|
||||
elif self._ray_pedal and CC.enabled and CS.out.cruiseState.enabled:
|
||||
if (self.frame - self.last_button_frame) * DT_CTRL > 0.1:
|
||||
can_sends.append(hyundaican.create_clu11(self.packer, self.frame, CS.clu11, Buttons.CANCEL, self.CP))
|
||||
self.last_button_frame = self.frame
|
||||
@@ -830,7 +830,7 @@ class CarController(CarControllerBase):
|
||||
pedal_ready = CS.ray_pedal_valid and CS.ray_pedal_state == 0
|
||||
pedal_active = (CC.longActive and pedal_ready and not CC.cruiseControl.override and
|
||||
not CS.out.gasPressed and not CS.out.brakePressed and
|
||||
not CS.out.cruiseState.enabled and CS.out.vEgo >= self.CP.minEnableSpeed)
|
||||
not CS.out.cruiseState.enabled)
|
||||
if pedal_active:
|
||||
target = float(np.clip(accel / CarControllerParams.ACCEL_MAX * RAY_PEDAL_COMMAND_CAP,
|
||||
0.0, RAY_PEDAL_COMMAND_CAP))
|
||||
|
||||
@@ -1556,6 +1556,7 @@ FW_VERSIONS = {
|
||||
},
|
||||
CAR.HYUNDAI_STARIA_4TH_GEN: {
|
||||
(Ecu.fwdCamera, 0x7c4, None): [
|
||||
b'\xf1\x00US4 MFC AT AUS RHD 1.00 1.04 99211-CG000 210819',
|
||||
b'\xf1\x00US4 MFC AT KOR LHD 1.00 1.06 99211-CG000 230524',
|
||||
],
|
||||
(Ecu.fwdRadar, 0x7d0, None): [
|
||||
|
||||
@@ -313,7 +313,7 @@ class CarInterface(CarInterfaceBase):
|
||||
ret.pcmCruise = False
|
||||
ret.radarUnavailable = True
|
||||
ret.autoResumeSng = False
|
||||
ret.minEnableSpeed = 5.0 # pedal-only: no commanded friction brake/standstill hold
|
||||
ret.minEnableSpeed = -1.0
|
||||
ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.LONG.value
|
||||
|
||||
# Car specific configuration overrides
|
||||
|
||||
@@ -1667,6 +1667,20 @@ class TestHyundaiFingerprint:
|
||||
assert exact
|
||||
assert matches == {candidate}
|
||||
|
||||
def test_staria_2023_australian_route_fw_exact_matches(self):
|
||||
route_fw = {
|
||||
(Ecu.fwdCamera, 0x7c4): b'\xf1\x00US4 MFC AT AUS RHD 1.00 1.04 99211-CG000 210819',
|
||||
(Ecu.fwdRadar, 0x7d0): b'\xf1\x00US4_ RDR ----- 1.00 1.00 99110-CG000 ',
|
||||
}
|
||||
car_fw = [
|
||||
CarParams.CarFw(ecu=ecu, fwVersion=version, address=address, subAddress=0, brand="hyundai")
|
||||
for (ecu, address), version in route_fw.items()
|
||||
]
|
||||
|
||||
exact, matches = match_fw_to_car(car_fw, "KMFYFX71MPU095311", allow_fuzzy=False, log=False)
|
||||
assert exact
|
||||
assert matches == {CAR.HYUNDAI_STARIA_4TH_GEN}
|
||||
|
||||
def test_kona_ev_non_scc_has_no_dedicated_fw_coverage(self):
|
||||
assert CAR.HYUNDAI_KONA_EV_NON_SCC not in FW_VERSIONS
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ def test_ray_pedal_fingerprint_isolation(candidate, fingerprint, has_pedal):
|
||||
if has_pedal:
|
||||
assert not CP.pcmCruise
|
||||
assert CP.safetyConfigs[-1].safetyParam == 0x9405
|
||||
assert CP.minEnableSpeed == 5.0
|
||||
assert CP.minEnableSpeed == -1.0
|
||||
assert not CP.autoResumeSng
|
||||
FPCP = CarInterface.get_starpilot_params(candidate, fingerprint, [], CP, SimpleNamespace())
|
||||
assert FPCP.canUsePedal
|
||||
@@ -128,13 +128,14 @@ def test_ray_without_pedal_keeps_native_gas_detection():
|
||||
assert ret.gasPressed
|
||||
|
||||
|
||||
def test_ray_controller_heartbeats_and_only_actuates_when_ready():
|
||||
@pytest.mark.parametrize("speed", [0.0, 0.1, 1.0, 4.9, 5.0, 12.0])
|
||||
def test_ray_controller_heartbeats_and_only_actuates_when_ready(speed):
|
||||
CP = CarInterface.get_params(CAR.KIA_RAY_EV, ray_fingerprint(), [], False, False, False, None)
|
||||
controller = CarController(DBC[CP.carFingerprint], CP)
|
||||
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS11", 0), ("CLU11", 0)], 0)
|
||||
CS = SimpleNamespace(
|
||||
lkas11=parser.vl["LKAS11"], clu11=parser.vl["CLU11"],
|
||||
out=SimpleNamespace(vEgo=12.0, gasPressed=False, brakePressed=False,
|
||||
out=SimpleNamespace(vEgo=speed, gasPressed=False, brakePressed=False,
|
||||
cruiseState=SimpleNamespace(enabled=False)),
|
||||
ray_pedal_valid=True, ray_pedal_state=5, is_metric=True,
|
||||
)
|
||||
@@ -168,3 +169,77 @@ def test_ray_controller_heartbeats_and_only_actuates_when_ready():
|
||||
hud, actuators, CS, CC, 2, 0)
|
||||
assert next(dat for addr, dat, bus in messages if addr == 0x200 and bus == 0)[:4] == bytes(4)
|
||||
assert any(addr == 0x4F1 and bus == 0 for addr, _, bus in messages) # cancel stock CC
|
||||
|
||||
CS.out.cruiseState.enabled = False
|
||||
CS.out.brakePressed = True
|
||||
assert pedal_msg(2.0, 20)[:4] == bytes(4)
|
||||
CS.out.brakePressed = False
|
||||
assert pedal_msg(2.0, 24)[4] & 0x80
|
||||
assert controller._ray_pedal_gas_last == pytest.approx(0.012)
|
||||
assert pedal_msg(2.0, 28)[4] & 0x80
|
||||
assert controller._ray_pedal_gas_last == pytest.approx(0.024)
|
||||
CS.out.brakePressed = True
|
||||
assert pedal_msg(2.0, 32)[:4] == bytes(4)
|
||||
assert controller._ray_pedal_gas_last == 0.0
|
||||
CS.out.brakePressed = False
|
||||
assert pedal_msg(2.0, 36)[4] & 0x80
|
||||
assert controller._ray_pedal_gas_last == pytest.approx(0.012)
|
||||
|
||||
CC.longActive = False
|
||||
assert pedal_msg(2.0, 40)[:4] == bytes(4)
|
||||
CC.longActive = True
|
||||
CC.cruiseControl.override = True
|
||||
assert pedal_msg(2.0, 44)[:4] == bytes(4)
|
||||
CC.cruiseControl.override = False
|
||||
CS.ray_pedal_valid = False
|
||||
assert pedal_msg(2.0, 48)[:4] == bytes(4)
|
||||
CS.ray_pedal_valid = True
|
||||
for fault in range(1, 6):
|
||||
CS.ray_pedal_state = fault
|
||||
assert pedal_msg(2.0, 48 + 4 * fault)[:4] == bytes(4)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("candidate", [CAR.KIA_RAY_EV, CAR.HYUNDAI_KONA_EV_NON_SCC])
|
||||
def test_ray_stock_cruise_cancellation_survives_accelerator_override(candidate):
|
||||
CP = CarInterface.get_params(candidate, ray_fingerprint(), [], False, False, False, None)
|
||||
controller = CarController(DBC[CP.carFingerprint], CP)
|
||||
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS11", 0), ("CLU11", 0)], 0)
|
||||
CS = SimpleNamespace(
|
||||
lkas11=parser.vl["LKAS11"], clu11=parser.vl["CLU11"],
|
||||
out=SimpleNamespace(vEgo=12.0, gasPressed=True, brakePressed=False,
|
||||
cruiseState=SimpleNamespace(enabled=True)),
|
||||
ray_pedal_valid=True, ray_pedal_state=0, is_metric=True,
|
||||
)
|
||||
CC = SimpleNamespace(
|
||||
enabled=True, longActive=False, latActive=True,
|
||||
cruiseControl=SimpleNamespace(cancel=False, resume=False, override=True),
|
||||
)
|
||||
hud = SimpleNamespace(
|
||||
visualAlert=CarControl.HUDControl.VisualAlert.none,
|
||||
leftLaneVisible=True, rightLaneVisible=True, leftLaneDepart=False, rightLaneDepart=False,
|
||||
)
|
||||
actuators = SimpleNamespace(longControlState=CarControl.Actuators.LongControlState.off)
|
||||
controller._create_can_redneck_button_messages = lambda _: []
|
||||
|
||||
def messages(frame):
|
||||
controller.frame = frame
|
||||
return controller.create_can_msgs(True, 0, False, 0.0, 2.0, False,
|
||||
hud, actuators, CS, CC, 2, 0)
|
||||
|
||||
def cancel_frames(msgs):
|
||||
return [dat for addr, dat, bus in msgs if addr == 0x4F1 and bus == 0 and dat[0] & 7 == 4]
|
||||
|
||||
msgs = messages(20)
|
||||
assert bool(cancel_frames(msgs)) is (candidate == CAR.KIA_RAY_EV)
|
||||
if candidate == CAR.KIA_RAY_EV:
|
||||
pedal = next(dat for addr, dat, bus in msgs if addr == 0x200 and bus == 0)
|
||||
assert pedal[:4] == bytes(4)
|
||||
assert not (pedal[4] & 0x80)
|
||||
assert not cancel_frames(messages(24)) # retain the existing cancellation rate limit
|
||||
assert cancel_frames(messages(32))
|
||||
|
||||
CS.out.cruiseState.enabled = False
|
||||
assert not cancel_frames(messages(44))
|
||||
CS.out.cruiseState.enabled = True
|
||||
CC.enabled = False
|
||||
assert not cancel_frames(messages(56)) # AOL alone must not cancel native cruise
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""One bounded Legacy AVH ON request; 0x32B is status, never a TX command."""
|
||||
|
||||
AVH_REQUEST = 0x6BB
|
||||
AVH_STATUS = 0x32B
|
||||
INPUTS = (AVH_REQUEST, AVH_STATUS, 0x40, 0x48, 0x13A, 0x174)
|
||||
|
||||
|
||||
def checksum(address, data):
|
||||
return ((address & 0xFF) + (address >> 8) + sum(data[1:])) & 0xFF
|
||||
|
||||
|
||||
def avh_request(template, step):
|
||||
if len(template) != 8 or checksum(AVH_REQUEST, template) != template[0] or template[2] & 3 or step not in (1, 2):
|
||||
raise ValueError("Invalid AVH template or counter step")
|
||||
data = bytearray(template)
|
||||
data[1] = (data[1] & 0xF0) | ((data[1] + step) & 0xF)
|
||||
data[2] |= 2
|
||||
data[0] = checksum(AVH_REQUEST, data)
|
||||
return AVH_REQUEST, bytes(data), 1
|
||||
|
||||
|
||||
class AvhStartup:
|
||||
def __init__(self):
|
||||
self.started = None
|
||||
self.last_time = None
|
||||
self.stable_since = None
|
||||
self.frames = {}
|
||||
self.done = False
|
||||
self.followup = None
|
||||
|
||||
def update(self, now, frames, enabled, can_valid, controls_active):
|
||||
if self.started is None:
|
||||
self.started = now
|
||||
if self.last_time is not None and now < self.last_time:
|
||||
self.done = True
|
||||
self.last_time = now
|
||||
if self.done:
|
||||
return []
|
||||
if now - self.started > 30 or controls_active:
|
||||
self.done = True
|
||||
return []
|
||||
|
||||
for address, (timestamp, data) in frames.items():
|
||||
if address not in INPUTS or timestamp <= 0:
|
||||
continue
|
||||
previous = self.frames.get(address)
|
||||
if previous and timestamp == previous[0]:
|
||||
continue
|
||||
if len(data) != 8 or checksum(address, data) != data[0] or timestamp > now or (previous and timestamp < previous[0]):
|
||||
self.done = True
|
||||
return []
|
||||
if (address == AVH_REQUEST and data[2] & 3) or (address == AVH_STATUS and data[5] & 0x20) or \
|
||||
(address == 0x48 and data[3] != 4) or (address == 0x40 and data[4]) or \
|
||||
(address == 0x13A and any((int.from_bytes(data, 'little') >> bit) & 0x1FFF for bit in (12, 25, 38, 51))):
|
||||
self.done = True
|
||||
return []
|
||||
if previous and (data[1] & 15) == (previous[1][1] & 15):
|
||||
continue # duplicate counters cannot refresh freshness
|
||||
# Controller snapshots can skip 50/100 Hz samples between updates. Panda
|
||||
# checks their full counter stream; require consecutive head-unit frames here.
|
||||
sequential = bool(previous and (address not in (AVH_REQUEST, AVH_STATUS) or
|
||||
(data[1] & 15) == ((previous[1][1] + 1) & 15)))
|
||||
self.frames[address] = (timestamp, data, sequential)
|
||||
|
||||
fresh = all(a in self.frames and self.frames[a][2] and
|
||||
0 <= now - self.frames[a][0] <= (1.5 if a == AVH_REQUEST else 0.3) for a in INPUTS)
|
||||
if not enabled or not can_valid or not fresh:
|
||||
self.stable_since = None
|
||||
if self.followup is not None:
|
||||
self.done = True
|
||||
return []
|
||||
throttle = self.frames[0x40][1]
|
||||
rpm = int.from_bytes(throttle[2:4], 'little') & 0x1FFF
|
||||
if rpm < 400 or not self.frames[0x174][1][2] & 8:
|
||||
self.stable_since = None
|
||||
if self.followup is not None:
|
||||
self.done = True
|
||||
return []
|
||||
if self.stable_since is None:
|
||||
self.stable_since = now
|
||||
if self.followup is not None:
|
||||
sent, timestamp, template = self.followup
|
||||
if now - sent > 0.075 or self.frames[AVH_REQUEST][0] != timestamp:
|
||||
self.done = True
|
||||
elif now - sent >= 0.05:
|
||||
self.done = True
|
||||
return [avh_request(template, 2)]
|
||||
return []
|
||||
if now - self.started < 10 or now - self.stable_since < 3:
|
||||
return []
|
||||
timestamp, template, _ = self.frames[AVH_REQUEST]
|
||||
if now - timestamp > 0.010:
|
||||
return []
|
||||
self.followup = (now, timestamp, template)
|
||||
return [avh_request(template, 1)]
|
||||
@@ -4,6 +4,7 @@ from opendbc.car import Bus, DT_CTRL, make_tester_present_msg, structs
|
||||
from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_std_steer_angle_limits, apply_steer_angle_limits_vm, common_fault_avoidance
|
||||
from opendbc.car.interfaces import CarControllerBase
|
||||
from opendbc.car.subaru import subarucan
|
||||
from opendbc.car.subaru.avh import AvhStartup
|
||||
from opendbc.car.subaru.values import CAR, DBC, GLOBAL_ES_ADDR, SUBARU_STOP_START_CARS, CanBus, CarControllerParams, SubaruFlags
|
||||
from opendbc.car.vehicle_model import VehicleModel
|
||||
|
||||
@@ -69,6 +70,7 @@ class CarController(CarControllerBase):
|
||||
self.stop_start_counter = 0
|
||||
self.stop_start_acknowledged = False
|
||||
self.last_redneck_button_frame = 0
|
||||
self.avh_startup = AvhStartup()
|
||||
|
||||
def _stop_start_off_request(self, CC, CS, starpilot_toggles):
|
||||
"""Send one bounded Subaru Stop/Start OFF request after ignition.
|
||||
@@ -215,7 +217,8 @@ class CarController(CarControllerBase):
|
||||
self.ascent_aol_arm_frames = _ASCENT_AOL_ARM_FRAMES if lkas_available else 0
|
||||
|
||||
if self.CP.carFingerprint == CAR.SUBARU_OUTBACK_2023:
|
||||
manual_handoff = False
|
||||
manual_handoff = not self.angle_lkas_active and \
|
||||
abs(getattr(CS.out, "steeringRateDeg", 0.0)) > _ANGLE_REENGAGE_MAX_STEER_RATE
|
||||
else:
|
||||
manual_handoff = self._angle_manual_handoff(CS, lkas_available)
|
||||
lkas_active = lkas_available and not manual_handoff
|
||||
@@ -307,6 +310,13 @@ class CarController(CarControllerBase):
|
||||
if stop_start_msg is not None:
|
||||
can_sends.append(stop_start_msg)
|
||||
|
||||
if self.CP.carFingerprint == CAR.SUBARU_LEGACY_2025:
|
||||
can_sends.extend(self.avh_startup.update(
|
||||
now_nanos / 1e9, getattr(CS, "avh_frames", {}),
|
||||
getattr(starpilot_toggles, "subaru_avh_on", False), getattr(CS.out, "canValid", False),
|
||||
CC.enabled or CC.latActive or CC.longActive,
|
||||
))
|
||||
|
||||
# *** steering ***
|
||||
if (self.frame % self.p.STEER_STEP) == 0:
|
||||
if self.CP.flags & SubaruFlags.LKAS_ANGLE:
|
||||
|
||||
@@ -4,8 +4,9 @@ from opendbc.can import CANDefine, CANParser
|
||||
from opendbc.car import Bus, create_button_events, structs
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.interfaces import CarStateBase
|
||||
from opendbc.car.subaru.values import DBC, CanBus, SUBARU_REDNECK_CRUISE_CARS, SUBARU_STOP_START_CARS, SubaruFlags
|
||||
from opendbc.car.subaru.values import CAR, DBC, CanBus, SUBARU_REDNECK_CRUISE_CARS, SUBARU_STOP_START_CARS, SubaruFlags
|
||||
from opendbc.car import CanSignalRateCalculator
|
||||
from opendbc.car.subaru.avh import INPUTS as AVH_INPUTS
|
||||
|
||||
ButtonType = structs.CarState.ButtonEvent.Type
|
||||
|
||||
@@ -26,6 +27,7 @@ class CarState(CarStateBase):
|
||||
self.dashlights_msg = {}
|
||||
self.dashlights_dat = b""
|
||||
self.stop_start_state = 0
|
||||
self.avh_frames = {}
|
||||
self.cruise_buttons_msg = {}
|
||||
self.cruise_buttons = {button: 0 for button in SUBARU_CRUISE_BUTTONS}
|
||||
|
||||
@@ -37,6 +39,9 @@ class CarState(CarStateBase):
|
||||
cp_angle = cp_main if self.CP.flags & SubaruFlags.D_PLATFORM else cp
|
||||
ret = structs.CarState()
|
||||
|
||||
if self.CP.carFingerprint == CAR.SUBARU_LEGACY_2025:
|
||||
self.avh_frames = {a: (cp_alt.ts_nanos[a]["CHECKSUM"] / 1e9, cp_alt.vl_raw[a]) for a in AVH_INPUTS}
|
||||
|
||||
if self.CP.carFingerprint in SUBARU_STOP_START_CARS:
|
||||
stop_start_cp = cp_alt if self.CP.flags & SubaruFlags.GLOBAL_GEN2 else cp
|
||||
self.dashlights_msg = copy.copy(stop_start_cp.vl["Dashlights"])
|
||||
@@ -177,11 +182,15 @@ class CarState(CarStateBase):
|
||||
|
||||
@staticmethod
|
||||
def get_can_parsers(CP):
|
||||
avh_messages = [(a, 0) for a in (0x6BB, 0x32B, 0x40, 0x48)] if CP.carFingerprint == CAR.SUBARU_LEGACY_2025 else []
|
||||
parsers = {
|
||||
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus.main_for_cp(CP)),
|
||||
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus.camera),
|
||||
Bus.alt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus.alt_for_cp(CP))
|
||||
Bus.alt: CANParser(DBC[CP.carFingerprint][Bus.pt], avh_messages, CanBus.alt_for_cp(CP))
|
||||
}
|
||||
if CP.flags & SubaruFlags.D_PLATFORM:
|
||||
parsers[Bus.main] = CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus.main)
|
||||
if CP.carFingerprint == CAR.SUBARU_LEGACY_2025:
|
||||
for address in AVH_INPUTS:
|
||||
parsers[Bus.alt].vl[address]
|
||||
return parsers
|
||||
|
||||
@@ -42,6 +42,8 @@ class CarInterface(CarInterfaceBase):
|
||||
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.D_PLATFORM_CAMERA.value
|
||||
if candidate in SUBARU_STOP_START_CARS:
|
||||
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.STOP_START_BUTTON.value
|
||||
if candidate == CAR.SUBARU_LEGACY_2025:
|
||||
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.AVH_STARTUP.value
|
||||
if candidate in (CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023, CAR.SUBARU_OUTBACK_2023):
|
||||
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.FIXED_ANGLE_LIMITS.value
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.car.subaru.avh import AVH_REQUEST, AVH_STATUS, INPUTS, AvhStartup, avh_request, checksum
|
||||
from opendbc.car.subaru.carcontroller import CarController
|
||||
from opendbc.car.subaru.interface import CarInterface
|
||||
from opendbc.car.subaru.values import CAR, SubaruSafetyFlags
|
||||
from opendbc.car import Bus
|
||||
|
||||
|
||||
def sample(address, counter):
|
||||
data = bytearray(8)
|
||||
data[1] = counter & 15
|
||||
if address == AVH_REQUEST:
|
||||
data[3], data[5], data[6] = 1, 0x80, 0x0E # captured Legacy payload, not Outback constants
|
||||
elif address == 0x40:
|
||||
data[2:4] = (800).to_bytes(2, 'little')
|
||||
elif address == 0x48:
|
||||
data[3] = 4
|
||||
elif address == 0x174:
|
||||
data[2] = 8
|
||||
data[0] = checksum(address, data)
|
||||
return bytes(data)
|
||||
|
||||
|
||||
def prepare(fast_counter_step=1):
|
||||
policy = AvhStartup()
|
||||
frames = {}
|
||||
for tick in range(101):
|
||||
now = 100 + tick / 10
|
||||
for address in INPUTS:
|
||||
if address != AVH_REQUEST or tick % 10 == 0:
|
||||
counter = tick // 10 if address == AVH_REQUEST else tick * (1 if address == AVH_STATUS else fast_counter_step)
|
||||
frames[address] = (now, sample(address, counter))
|
||||
sent = policy.update(now, frames, True, True, False)
|
||||
if tick < 100:
|
||||
assert sent == []
|
||||
assert sent == [avh_request(frames[AVH_REQUEST][1], 1)]
|
||||
return policy, frames
|
||||
|
||||
|
||||
def test_captured_legacy_press_bytes():
|
||||
template = bytes.fromhex('5b0b000100800e00')
|
||||
assert avh_request(template, 1) == (0x6BB, bytes.fromhex('5e0c020100800e00'), 1)
|
||||
assert avh_request(template, 2) == (0x6BB, bytes.fromhex('5f0d020100800e00'), 1)
|
||||
wrap = sample(AVH_REQUEST, 15)
|
||||
assert avh_request(wrap, 1)[1][1] == 0
|
||||
assert avh_request(wrap, 2)[1][1] == 1
|
||||
|
||||
|
||||
def test_two_frames_only_and_no_retry():
|
||||
policy, frames = prepare()
|
||||
assert policy.update(110.04, frames, True, True, False) == []
|
||||
assert policy.update(110.06, frames, True, True, False) == [avh_request(frames[AVH_REQUEST][1], 2)]
|
||||
assert policy.update(110.07, frames, True, True, False) == []
|
||||
assert policy.update(111, frames, True, True, False) == []
|
||||
|
||||
|
||||
def test_controller_snapshots_may_skip_fast_can_samples():
|
||||
policy, frames = prepare(fast_counter_step=2)
|
||||
assert policy.update(110.06, frames, True, True, False) == [avh_request(frames[AVH_REQUEST][1], 2)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('reason', ['late', 'new_template', 'manual', 'ack', 'moving', 'gas', 'gear', 'invalid', 'disabled', 'engaged', 'stale'])
|
||||
def test_followup_aborts_permanently(reason):
|
||||
policy, frames = prepare()
|
||||
address, offset, value = {
|
||||
'manual': (AVH_REQUEST, 2, 1), 'ack': (AVH_STATUS, 5, 32),
|
||||
'moving': (0x13A, 2, 1), 'gas': (0x40, 4, 1), 'gear': (0x48, 3, 3),
|
||||
'new_template': (AVH_REQUEST, 1, 11),
|
||||
}.get(reason, (None, None, None))
|
||||
if address is not None:
|
||||
data = bytearray(frames[address][1])
|
||||
data[1] = (data[1] + 1) & 15
|
||||
data[offset] = value
|
||||
data[0] = checksum(address, data)
|
||||
frames[address] = (110.05, bytes(data))
|
||||
if reason == 'stale':
|
||||
frames[0x40] = (109, frames[0x40][1])
|
||||
now = 110.08 if reason == 'late' else 110.06
|
||||
assert policy.update(now, frames, reason != 'disabled', reason != 'invalid', reason == 'engaged') == []
|
||||
assert policy.done
|
||||
assert policy.update(111, frames, True, True, False) == []
|
||||
|
||||
|
||||
def test_only_legacy_has_avh_safety_permission():
|
||||
for car in CAR:
|
||||
cp = CarInterface.get_non_essential_params(car)
|
||||
assert bool(cp.safetyConfigs[0].safetyParam & SubaruSafetyFlags.AVH_STARTUP) == (car == CAR.SUBARU_LEGACY_2025)
|
||||
|
||||
|
||||
def test_existing_required_messages_keep_alive_checks():
|
||||
cp = CarInterface.get_non_essential_params(CAR.SUBARU_LEGACY_2025)
|
||||
parser = CarInterface.CarState.get_can_parsers(cp)[Bus.alt]
|
||||
assert not parser.message_states[0x13A].ignore_alive
|
||||
assert not parser.message_states[0x174].ignore_alive
|
||||
assert parser.message_states[AVH_REQUEST].ignore_alive
|
||||
assert parser.message_states[AVH_STATUS].ignore_alive
|
||||
|
||||
|
||||
def test_controller_sends_only_when_opted_in():
|
||||
cp = CarInterface.get_non_essential_params(CAR.SUBARU_LEGACY_2025)
|
||||
controller = CarController({}, cp)
|
||||
cc = SimpleNamespace(enabled=False, latActive=False, longActive=False,
|
||||
actuators=SimpleNamespace(as_builder=lambda: SimpleNamespace(steeringAngleDeg=0)),
|
||||
hudControl=SimpleNamespace(leadVisible=False), cruiseControl=SimpleNamespace(cancel=False))
|
||||
cs = SimpleNamespace(out=SimpleNamespace(canValid=True), avh_frames={})
|
||||
toggles = SimpleNamespace(subaru_stop_start_off=False, subaru_avh_on=False, subaru_sng=False)
|
||||
controller.frame = 1
|
||||
_, sent = controller.update(cc, cs, 100_000_000_000, toggles)
|
||||
assert not any(m[0] in (AVH_REQUEST, AVH_STATUS) for m in sent)
|
||||
@@ -806,7 +806,7 @@ def test_outback_manual_steering_keeps_cooperative_angle_request():
|
||||
CS = SimpleNamespace(out=SimpleNamespace(
|
||||
vEgoRaw=0.9,
|
||||
steeringAngleDeg=-57.0,
|
||||
steeringRateDeg=-45.0,
|
||||
steeringRateDeg=0.0,
|
||||
steeringTorque=0.0,
|
||||
steeringPressed=True,
|
||||
gearShifter=structs.CarState.GearShifter.drive,
|
||||
@@ -815,6 +815,7 @@ def test_outback_manual_steering_keeps_cooperative_angle_request():
|
||||
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("ES_LKAS_ANGLE", 0)], CanBus.main)
|
||||
|
||||
for frame, steering_torque in enumerate((-79.0, -81.0, -170.0, -250.0, -250.0, 79.0, 81.0, 170.0, 250.0, 250.0), start=1):
|
||||
CS.out.steeringRateDeg = 0.0 if frame == 1 else -45.0
|
||||
CS.out.steeringTorque = steering_torque
|
||||
CS.out.steeringPressed = abs(steering_torque) > 80.0
|
||||
msg = controller.lateral_angle(CC, CS)
|
||||
@@ -825,6 +826,27 @@ def test_outback_manual_steering_keeps_cooperative_angle_request():
|
||||
assert controller._lkas_status_active(CC)
|
||||
|
||||
|
||||
def test_outback_waits_for_manual_turn_to_settle_before_reentry():
|
||||
CP = CarInterface.get_non_essential_params(CAR.SUBARU_OUTBACK_2023)
|
||||
controller = CarController({}, CP)
|
||||
CC = SimpleNamespace(enabled=False, latActive=True, actuators=SimpleNamespace(steeringAngleDeg=-80.0))
|
||||
CS = SimpleNamespace(out=SimpleNamespace(
|
||||
vEgoRaw=7.3, steeringAngleDeg=-121.47, steeringRateDeg=126.5,
|
||||
gearShifter=structs.CarState.GearShifter.drive, standstill=False,
|
||||
))
|
||||
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("ES_LKAS_ANGLE", 0)], CanBus.main)
|
||||
for frame, (angle, rate, active) in enumerate([
|
||||
(-121.47, 126.5, False), (-117.96, 122.5, False), (-88.65, 112.0, False),
|
||||
(-0.24, 0.0, True),
|
||||
], start=1):
|
||||
CS.out.steeringAngleDeg = angle
|
||||
CS.out.steeringRateDeg = rate
|
||||
parser.update([(frame, [controller.lateral_angle(CC, CS)])])
|
||||
assert bool(parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"]) == active
|
||||
if not active:
|
||||
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(angle, abs=0.01)
|
||||
|
||||
|
||||
def test_ascent_hud_waits_for_angle_request():
|
||||
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
|
||||
controller = CarController({}, CP)
|
||||
|
||||
@@ -90,6 +90,7 @@ class SubaruSafetyFlags(IntFlag):
|
||||
FIXED_ANGLE_LIMITS = 128
|
||||
STOP_START_BUTTON = 256
|
||||
REDNECK_CRUISE = 512
|
||||
AVH_STARTUP = 1024
|
||||
LEGACY_2025_ANGLE_LIMITS = FIXED_ANGLE_LIMITS
|
||||
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ class CarController(CarControllerBase):
|
||||
def _update_preap(self, CC, CS):
|
||||
actuators = CC.actuators
|
||||
can_sends = []
|
||||
lat_active = CC.latActive and CS.hands_on_level < 3
|
||||
lat_active = CC.latActive and CS.hands_on_level < 3 and getattr(CS, "preap_lateral_authorized", False)
|
||||
|
||||
if CC.cruiseControl.cancel and CS.cruiseEnabled:
|
||||
CS.cruiseEnabled = False
|
||||
@@ -169,8 +169,10 @@ class CarController(CarControllerBase):
|
||||
CS.engagement.pedal_speed_kph = 0.0
|
||||
|
||||
if self.frame % 2 == 0:
|
||||
requested_angle = float(np.clip(actuators.steeringAngleDeg,
|
||||
CS.out.steeringAngleDeg - 20., CS.out.steeringAngleDeg + 20.))
|
||||
self.apply_angle_last = apply_steer_angle_limits_vm(
|
||||
actuators.steeringAngleDeg, self.apply_angle_last, CS.out.vEgoRaw, CS.out.steeringAngleDeg,
|
||||
requested_angle, self.apply_angle_last, CS.out.vEgoRaw, CS.out.steeringAngleDeg,
|
||||
lat_active, CarControllerParams, self.VM,
|
||||
)
|
||||
cntr = (self.frame // 2) % 16
|
||||
|
||||
@@ -13,6 +13,8 @@ class PreAPEngagement:
|
||||
self.enableDoublePull = double_pull_enabled
|
||||
self.double_pull_window_ms = double_pull_window_ms
|
||||
self.cruiseEnabled = False
|
||||
self.lateralEnabled = False
|
||||
self.lateralRearmRequired = False
|
||||
self.enableLongControl = False
|
||||
self.enableJustCC = False
|
||||
self.pending_enable = False
|
||||
@@ -28,6 +30,8 @@ class PreAPEngagement:
|
||||
|
||||
def handle_steering_disengage(self, steering_disengage: bool) -> None:
|
||||
if steering_disengage and not self.prev_steering_disengage:
|
||||
self.lateralEnabled = False
|
||||
self.lateralRearmRequired = True
|
||||
self.cruiseEnabled = False
|
||||
self.enableLongControl = False
|
||||
self.enableJustCC = False
|
||||
@@ -45,6 +49,8 @@ class PreAPEngagement:
|
||||
button_events: list[structs.CarState.ButtonEvent] = []
|
||||
|
||||
if cruise_buttons == CruiseButtons.MAIN and prev_cruise_buttons != CruiseButtons.MAIN:
|
||||
self.lateralEnabled = True
|
||||
self.lateralRearmRequired = False
|
||||
if self.enableDoublePull:
|
||||
self._handle_double_pull(curr_time_ms, v_ego, speed_units, use_pedal, pedal_long_allowed, long_control_allowed, di_cruise_state)
|
||||
else:
|
||||
@@ -75,6 +81,8 @@ class PreAPEngagement:
|
||||
def check_can_engage(self, door_open: bool, gear_shifter, seatbelt_unlatched: bool) -> bool:
|
||||
can_engage = not door_open and gear_shifter == structs.CarState.GearShifter.drive and not seatbelt_unlatched
|
||||
if not can_engage:
|
||||
self.lateralEnabled = False
|
||||
self.lateralRearmRequired = True
|
||||
self.cruiseEnabled = False
|
||||
self.enableLongControl = False
|
||||
self.enableJustCC = False
|
||||
@@ -118,6 +126,8 @@ class PreAPEngagement:
|
||||
((curr_time_ms - self.preap_last_cc_spoof_ms) < SPOOF_ECHO_WINDOW_MS)
|
||||
be.type = ButtonType.unknown if is_echo else ButtonType.cancel
|
||||
if not is_echo:
|
||||
self.lateralEnabled = False
|
||||
self.lateralRearmRequired = True
|
||||
self.cruiseEnabled = False
|
||||
self.enableLongControl = False
|
||||
self.enableJustCC = False
|
||||
@@ -145,4 +155,3 @@ class PreAPEngagement:
|
||||
def _capture_target_speed(v_ego: float, speed_units: str) -> float:
|
||||
speed_uom_kph = CV.MPH_TO_KPH if speed_units == "MPH" else 1.0
|
||||
return max(int(v_ego * CV.MS_TO_KPH / speed_uom_kph + 0.5) * speed_uom_kph, 0.0)
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
from opendbc.car import structs
|
||||
from opendbc.safety import ALTERNATIVE_EXPERIENCE
|
||||
|
||||
|
||||
def preap_lateral_authorized(CP, CS, panda_states, panda_states_valid: bool) -> bool:
|
||||
"""Match Pre-AP's existing safety authorization without treating software CC availability as ACC main."""
|
||||
if not panda_states_valid or CS.out.gearShifter != structs.CarState.GearShifter.drive or CS.out.doorOpen or CS.out.steeringDisengage:
|
||||
return False
|
||||
if CS.engagement.lateralRearmRequired:
|
||||
return False
|
||||
config = CP.safetyConfigs[0]
|
||||
matching = [p for p in panda_states if p.safetyModel == config.safetyModel and p.safetyParam == config.safetyParam]
|
||||
if len(matching) != 1 or matching[0].safetyRxChecksInvalid:
|
||||
return False
|
||||
panda = matching[0]
|
||||
# Physical cancel/override/gear changes clear this latch immediately, whereas
|
||||
# Panda telemetry can lag. Longitudinal software cancellation leaves it intact.
|
||||
stalk_authorized = CS.engagement.lateralEnabled and panda.controlsAllowed
|
||||
stock_main = CS.di_cruise_state in ("STANDBY", "ENABLED", "STANDSTILL", "OVERRIDE", "PRE_FAULT", "PRE_CANCEL")
|
||||
aol_authorized = bool(panda.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL) and stock_main
|
||||
return bool(stalk_authorized or aol_authorized)
|
||||
@@ -0,0 +1,36 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.tesla.carcontroller import CarController
|
||||
from opendbc.car.tesla.interface import CarInterface
|
||||
from opendbc.car.tesla.values import CAR, DBC
|
||||
|
||||
|
||||
@pytest.mark.parametrize('direction', [-1., 1.])
|
||||
def test_preap_stalled_rack_request_stays_within_legacy_tracking_envelope(direction):
|
||||
cp = CarInterface.get_non_essential_params(CAR.TESLA_MODEL_S_PREAP)
|
||||
controller = CarController(DBC[cp.carFingerprint], cp)
|
||||
controller.stock_cc = None
|
||||
cs = SimpleNamespace(out=SimpleNamespace(vEgoRaw=3., steeringAngleDeg=0.),
|
||||
hands_on_level=0, preap_lateral_authorized=True, cruiseEnabled=False)
|
||||
cc = structs.CarControl.new_message()
|
||||
cc.latActive = True
|
||||
cc.actuators.steeringAngleDeg = direction * 100.
|
||||
previous = 0.
|
||||
for frame in range(100):
|
||||
output, _ = controller.update(cc.as_reader(), cs, frame * 10000000, None)
|
||||
assert abs(output.steeringAngleDeg) <= 20.
|
||||
assert abs(output.steeringAngleDeg - previous) <= 5.
|
||||
previous = output.steeringAngleDeg
|
||||
assert previous == direction * 20.
|
||||
|
||||
cs.out.steeringAngleDeg = -direction * 50.
|
||||
output, _ = controller.update(cc.as_reader(), cs, 1000000000, None)
|
||||
assert abs(output.steeringAngleDeg - previous) <= 5.
|
||||
|
||||
cc.latActive = False
|
||||
controller.frame = 102
|
||||
output, _ = controller.update(cc.as_reader(), cs, 1020000000, None)
|
||||
assert output.steeringAngleDeg == cs.out.steeringAngleDeg
|
||||
@@ -47,6 +47,7 @@ TOYOTA_AUTO_HOLD_ACTIVATION_FRAMES = 100
|
||||
# EPS faults if you apply torque while the steering rate is above 100 deg/s for too long
|
||||
MAX_STEER_RATE = 100 # deg/s
|
||||
MAX_STEER_RATE_FRAMES = 17 # tx control frames needed before torque can be cut
|
||||
COROLLA_MAX_STEER_RATE = 80
|
||||
|
||||
# EPS allows user torque above threshold for 50 frames before permanently faulting
|
||||
MAX_USER_TORQUE = 500
|
||||
@@ -77,12 +78,12 @@ def should_bypass_toyota_long_pid(CP, starpilot_toggles=None) -> bool:
|
||||
) or highlander_sdsu)
|
||||
|
||||
|
||||
def get_toyota_lat_active(car_fingerprint, requested_active: bool, steering_torque: float,
|
||||
steering_pressed: bool) -> bool:
|
||||
if not requested_active or abs(steering_torque) >= MAX_USER_TORQUE:
|
||||
return False
|
||||
def get_toyota_lat_active(requested_active: bool, steering_torque: float) -> bool:
|
||||
return requested_active and abs(steering_torque) < MAX_USER_TORQUE
|
||||
|
||||
return not (car_fingerprint == CAR.TOYOTA_COROLLA_TSS2 and steering_pressed)
|
||||
|
||||
def get_toyota_steer_rate_limit(car_fingerprint) -> int:
|
||||
return COROLLA_MAX_STEER_RATE if car_fingerprint == CAR.TOYOTA_COROLLA_TSS2 else MAX_STEER_RATE
|
||||
|
||||
|
||||
def supports_toyota_auto_hold(CP, auto_hold_enabled: bool) -> bool:
|
||||
@@ -252,6 +253,7 @@ class CarController(CarControllerBase):
|
||||
self.standstill_req = False
|
||||
self.permit_braking = True
|
||||
self.steer_rate_counter = 0
|
||||
self.steer_rate_limit = get_toyota_steer_rate_limit(self.CP.carFingerprint)
|
||||
self.distance_button = 0
|
||||
|
||||
# *** start long control state ***
|
||||
@@ -343,8 +345,7 @@ class CarController(CarControllerBase):
|
||||
stopping = actuators.longControlState == LongCtrlState.stopping
|
||||
hud_control = CC.hudControl
|
||||
pcm_cancel_cmd = CC.cruiseControl.cancel
|
||||
lat_active = get_toyota_lat_active(self.CP.carFingerprint, CC.latActive,
|
||||
CS.out.steeringTorque, CS.out.steeringPressed)
|
||||
lat_active = get_toyota_lat_active(CC.latActive, CS.out.steeringTorque)
|
||||
|
||||
if len(CC.orientationNED) == 3:
|
||||
self.pitch.update(CC.orientationNED[1])
|
||||
@@ -371,7 +372,7 @@ class CarController(CarControllerBase):
|
||||
|
||||
# >100 degree/sec steering fault prevention
|
||||
self.steer_rate_counter, apply_steer_req = common_fault_avoidance(
|
||||
abs(CS.out.steeringRateDeg) >= MAX_STEER_RATE, lat_active,
|
||||
abs(CS.out.steeringRateDeg) >= self.steer_rate_limit, lat_active,
|
||||
self.steer_rate_counter, MAX_STEER_RATE_FRAMES,
|
||||
)
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ from hypothesis import given, settings, strategies as st
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.can import CANPacker, CANParser
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car.lateral import common_fault_avoidance
|
||||
from opendbc.car.fw_versions import build_fw_dict, match_fw_to_car
|
||||
from opendbc.car.toyota import toyotacan
|
||||
from opendbc.car.toyota.carcontroller import CarController, get_camry_hybrid_feedforward, get_long_tune, get_prius_feedforward, \
|
||||
get_prius_positive_feedforward_scale, \
|
||||
get_rav4_interceptor_pedal_scale, \
|
||||
get_toyota_lat_active, \
|
||||
get_toyota_lat_active, get_toyota_steer_rate_limit, \
|
||||
MAX_STEER_RATE, MAX_STEER_RATE_FRAMES, MAX_USER_TORQUE, \
|
||||
limit_interceptor_pcm_accel, \
|
||||
limit_interceptor_stopping_accel, limit_no_lead_cruise_sign_flip, \
|
||||
limit_prius_stopping_accel, should_bypass_toyota_long_pid, supports_toyota_auto_hold, \
|
||||
@@ -735,14 +737,43 @@ class TestToyotaFingerprint:
|
||||
|
||||
|
||||
class TestToyotaCarController:
|
||||
def test_corolla_tss2_hands_off_immediately_when_driver_is_steering(self):
|
||||
assert not get_toyota_lat_active(CAR.TOYOTA_COROLLA_TSS2, True, 117, True)
|
||||
@pytest.mark.parametrize("driver_torque", [-191, -117, -99, 99, 117, 191])
|
||||
def test_toyota_assisting_driver_keeps_lateral_active(self, driver_torque):
|
||||
assert get_toyota_lat_active(True, driver_torque)
|
||||
|
||||
def test_corolla_tss2_stays_active_without_driver_input(self):
|
||||
assert get_toyota_lat_active(CAR.TOYOTA_COROLLA_TSS2, True, 99, False)
|
||||
@pytest.mark.parametrize("driver_torque", [-MAX_USER_TORQUE, MAX_USER_TORQUE, MAX_USER_TORQUE + 1])
|
||||
def test_toyota_high_driver_torque_still_disables_lateral(self, driver_torque):
|
||||
assert not get_toyota_lat_active(True, driver_torque)
|
||||
|
||||
def test_toyota_driver_handoff_behavior_is_corolla_only(self):
|
||||
assert get_toyota_lat_active(CAR.TOYOTA_RAV4_TSS2, True, 117, True)
|
||||
def test_toyota_inactive_request_stays_inactive(self):
|
||||
assert not get_toyota_lat_active(False, 0)
|
||||
|
||||
def test_toyota_assisting_driver_retains_rate_fault_protection(self):
|
||||
counter = 0
|
||||
requests = []
|
||||
for _ in range(36):
|
||||
counter, request = common_fault_avoidance(
|
||||
150 >= MAX_STEER_RATE, get_toyota_lat_active(True, 117), counter, MAX_STEER_RATE_FRAMES,
|
||||
)
|
||||
requests.append(request)
|
||||
assert requests == ([True] * 17 + [False]) * 2
|
||||
|
||||
@pytest.mark.parametrize("candidate", list(CAR))
|
||||
def test_steer_rate_margin_is_corolla_only(self, candidate):
|
||||
expected = 80 if candidate == CAR.TOYOTA_COROLLA_TSS2 else MAX_STEER_RATE
|
||||
assert get_toyota_steer_rate_limit(candidate) == expected
|
||||
|
||||
@pytest.mark.parametrize("direction", [-1, 1])
|
||||
def test_corolla_rate_margin_preserves_request_spacing(self, direction):
|
||||
counter = 0
|
||||
requests = []
|
||||
for rate in [0] * 30 + [90 * direction] * 36 + [0] * 30:
|
||||
counter, request = common_fault_avoidance(
|
||||
abs(rate) >= get_toyota_steer_rate_limit(CAR.TOYOTA_COROLLA_TSS2),
|
||||
get_toyota_lat_active(True, 117 * direction), counter, MAX_STEER_RATE_FRAMES,
|
||||
)
|
||||
requests.append(request)
|
||||
assert requests == [True] * 30 + ([True] * 17 + [False]) * 2 + [True] * 30
|
||||
|
||||
@staticmethod
|
||||
def _make_controller(*, standstill_req=False, last_standstill=False):
|
||||
|
||||
@@ -170,8 +170,6 @@ class CarController(CarControllerBase):
|
||||
# convention = driver pushing right → yields right authority
|
||||
# (LOOSELY/+ arm), retains left (INV/- arm).
|
||||
# Yield arm scales with |drv| above OVERRIDE_THRESH — strong presses
|
||||
# (potholes, hard corrections) cross past zero so EPS hands the wheel
|
||||
# to the driver in their direction.
|
||||
excess = max(0.0, self.lca_auth_drv_mag_filt - float(P.LCA_AUTH_OVERRIDE_ENTER))
|
||||
yield_signed = float(P.LCA_AUTH_YIELD_BASE) - P.LCA_AUTH_YIELD_SLOPE * excess
|
||||
yield_signed = max(float(P.LCA_AUTH_YIELD_MIN), min(yield_signed, float(P.LCA_AUTH_YIELD_BASE)))
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from collections import defaultdict
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.car.volvo.carcontroller import CarController
|
||||
from opendbc.car.volvo.helpers import checksum_lca_5_message
|
||||
from opendbc.car.volvo.interface import CarInterface
|
||||
from opendbc.car.volvo.values import CAR, DBC
|
||||
from opendbc.car.volvo.volvocan import create_c1_checksum
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
|
||||
def _zero_message():
|
||||
@@ -70,6 +73,35 @@ def test_controller_relays_stock_lca5_angle_when_inactive():
|
||||
assert abs(raw * 0.05596 - 12.0) < 0.1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fingerprint", [CAR.POLESTAR_2, CAR.VOLVO_XC40_RECHARGE])
|
||||
@pytest.mark.parametrize("driver_torque", [20.0, -20.0, 128.0, -127.0])
|
||||
def test_override_lca_stream_passes_safety_and_recovers(fingerprint, driver_torque):
|
||||
cp = CarInterface.get_non_essential_params(fingerprint)
|
||||
controller = CarController(DBC[cp.carFingerprint], cp)
|
||||
cs = _state()
|
||||
cc = SimpleNamespace(latActive=True, actuators=_Actuators())
|
||||
safety = libsafety_py.libsafety
|
||||
config = cp.safetyConfigs[0]
|
||||
assert safety.set_safety_hooks(config.safetyModel.raw, config.safetyParam) == 0
|
||||
safety.init_tests()
|
||||
safety.set_controls_allowed(True)
|
||||
|
||||
for active, torque in [(True, 0), (True, driver_torque), (True, -driver_torque),
|
||||
(True, 0), (False, 0), (True, 0)]:
|
||||
cc.latActive = active
|
||||
cs.out.steeringTorque = torque
|
||||
for _ in range(350):
|
||||
_, messages = controller.update(cc, cs, 0, None)
|
||||
address, data, bus = next(msg for msg in messages if msg[0] == 0x58)
|
||||
assert safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data)), (
|
||||
active, torque, controller.frame, controller.lca_auth_pos, controller.lca_auth_neg)
|
||||
if active and torque == 0:
|
||||
assert controller.lca_auth_pos == 614
|
||||
assert controller.lca_auth_neg == -614
|
||||
elif active:
|
||||
assert min(abs(controller.lca_auth_pos), abs(controller.lca_auth_neg)) == 0
|
||||
|
||||
|
||||
def _c1_state():
|
||||
return SimpleNamespace(
|
||||
out=SimpleNamespace(steeringAngleDeg=10.0, vEgo=12.0, vEgoRaw=12.0),
|
||||
|
||||
@@ -71,14 +71,12 @@ class CarControllerParams:
|
||||
# (potholes, lane corrections) get full yield while light sustained pressure
|
||||
# only gets a soft yield. yield_signed = YIELD_BASE − YIELD_SLOPE *
|
||||
# max(0, drv_mag_filt − OVERRIDE_ENTER), clamped to [YIELD_MIN, YIELD_BASE].
|
||||
# At |drv|=7 (just over threshold): yield = +60 (light resistance).
|
||||
# At |drv|=14: yield ≈ -4 (crosses past zero — EPS hands wheel to driver).
|
||||
# drv_mag_filt is a low-pass of |drv| (alpha=0.04, ~250 ms time constant) —
|
||||
# without it, 1-2 unit driver-torque jitter became ~10 unit yield-arm jitter
|
||||
# which PSCM converted to felt ripple at sustained co-steering pressure.
|
||||
LCA_AUTH_YIELD_BASE = 60 # yield-arm magnitude at the override threshold
|
||||
LCA_AUTH_YIELD_SLOPE = 8 # counts of yield reduction per unit |drv torque| above threshold
|
||||
LCA_AUTH_YIELD_MIN = -30 # cap how far past zero the yield arm can go (full hand-over)
|
||||
LCA_AUTH_YIELD_MIN = 0 # yield authority without crossing the safety sign boundary
|
||||
LCA_AUTH_YIELD_LP_ALPHA = 0.04 # LP-filter coefficient on |drv| for yield calc (~250 ms tau)
|
||||
LCA_AUTH_SPLIT = 200 # symmetric → asymmetric handover
|
||||
LCA_AUTH_REBUILD_RATE = 230 # counts/s (≈ 2.7 s rebuild from 0 to 614)
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
CM_ "IMPORT _subaru_global.dbc";
|
||||
|
||||
BO_ 1723 AVH_Request: 8 XXX
|
||||
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
|
||||
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
|
||||
SG_ REQUEST : 16|2@1+ (1,0) [0|3] "" XXX
|
||||
|
||||
BO_ 811 AVH_Status: 8 XXX
|
||||
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
|
||||
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
|
||||
SG_ ENABLED : 45|1@1+ (1,0) [0|1] "" XXX
|
||||
|
||||
BO_ 72 Transmission: 8 XXX
|
||||
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
|
||||
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
|
||||
|
||||
@@ -307,6 +307,16 @@ VAL_ 544 AEB_Status 12 "AEB related" 8 "AEB actuation" 4 "AEB related" 0 "No AEB
|
||||
|
||||
CM_ "subaru_global_2017.dbc starts here";
|
||||
|
||||
BO_ 1723 AVH_Request: 8 XXX
|
||||
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
|
||||
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
|
||||
SG_ REQUEST : 16|2@1+ (1,0) [0|3] "" XXX
|
||||
|
||||
BO_ 811 AVH_Status: 8 XXX
|
||||
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
|
||||
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
|
||||
SG_ ENABLED : 45|1@1+ (1,0) [0|1] "" XXX
|
||||
|
||||
BO_ 72 Transmission: 8 XXX
|
||||
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
|
||||
SG_ COUNTER : 8|4@1+ (1,0) [0|15] "" XXX
|
||||
|
||||
@@ -136,6 +136,8 @@ static uint32_t subaru_compute_checksum(const CANPacket_t *msg) {
|
||||
return checksum;
|
||||
}
|
||||
|
||||
#include "opendbc/safety/modes/subaru_avh.h"
|
||||
|
||||
static void subaru_rx_hook(const CANPacket_t *msg) {
|
||||
const unsigned int alt_main_bus = subaru_gen2 ? SUBARU_ALT_BUS : SUBARU_MAIN_BUS;
|
||||
const unsigned int status_bus = subaru_gen2 ? SUBARU_ALT_BUS : SUBARU_CAM_BUS;
|
||||
@@ -308,6 +310,10 @@ static bool subaru_tx_hook(const CANPacket_t *msg) {
|
||||
violation |= subaru_get_checksum(msg) != subaru_compute_checksum(msg);
|
||||
}
|
||||
|
||||
if (msg->addr == 0x6BBU) {
|
||||
violation |= !subaru_avh_tx(msg);
|
||||
}
|
||||
|
||||
if (violation){
|
||||
tx = false;
|
||||
}
|
||||
@@ -315,6 +321,12 @@ static bool subaru_tx_hook(const CANPacket_t *msg) {
|
||||
}
|
||||
|
||||
static safety_config subaru_init(uint16_t param) {
|
||||
static const CanMsg SUBARU_LEGACY_AVH_TX_MSGS[] = {
|
||||
SUBARU_BASE_TX_MSGS(SUBARU_ALT_BUS, MSG_SUBARU_ES_LKAS_ANGLE)
|
||||
SUBARU_COMMON_TX_MSGS(SUBARU_ALT_BUS)
|
||||
SUBARU_STOP_START_TX_MSGS(SUBARU_ALT_BUS)
|
||||
{0x6BBU, SUBARU_ALT_BUS, 8, .check_relay = false},
|
||||
};
|
||||
static const CanMsg SUBARU_TX_MSGS[] = {
|
||||
SUBARU_BASE_TX_MSGS(SUBARU_MAIN_BUS, MSG_SUBARU_ES_LKAS)
|
||||
SUBARU_COMMON_TX_MSGS(SUBARU_MAIN_BUS)
|
||||
@@ -455,12 +467,22 @@ static safety_config subaru_init(uint16_t param) {
|
||||
subaru_stop_and_go ? BUILD_SAFETY_CFG(subaru_rx_checks, SUBARU_STOP_AND_GO_TX_MSGS) : \
|
||||
BUILD_SAFETY_CFG(subaru_rx_checks, SUBARU_TX_MSGS);
|
||||
}
|
||||
bool avh_enabled = false;
|
||||
#ifdef ALLOW_DEBUG
|
||||
avh_enabled = GET_FLAG(param, 1024U) && subaru_gen2 && subaru_lkas_angle && subaru_fixed_angle_limits &&
|
||||
subaru_stop_start_button && !subaru_d_platform && !GET_FLAG(param, 2U) && !subaru_redneck_cruise;
|
||||
#endif
|
||||
subaru_avh_init(avh_enabled);
|
||||
if (avh_enabled) {
|
||||
ret = BUILD_SAFETY_CFG(subaru_gen2_lkas_angle_rx_checks, SUBARU_LEGACY_AVH_TX_MSGS);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
const safety_hooks subaru_hooks = {
|
||||
.init = subaru_init,
|
||||
.rx = subaru_rx_hook,
|
||||
.rx_all = subaru_avh_rx,
|
||||
.tx = subaru_tx_hook,
|
||||
.get_counter = subaru_get_counter,
|
||||
.get_checksum = subaru_get_checksum,
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#pragma once
|
||||
|
||||
// Legacy startup AVH only. Never transmit the 0x32B status message.
|
||||
static const unsigned int SUBARU_AVH_INPUTS[] = {0x6BBU, 0x32BU, 0x40U, 0x48U, 0x13AU, 0x174U};
|
||||
static uint8_t subaru_avh_data[6][8];
|
||||
static uint32_t subaru_avh_ts[6];
|
||||
static bool subaru_avh_seen[6];
|
||||
static bool subaru_avh_seq[6];
|
||||
static bool subaru_avh_enabled;
|
||||
static bool subaru_avh_done;
|
||||
static unsigned int subaru_avh_count;
|
||||
static uint32_t subaru_avh_start;
|
||||
static uint32_t subaru_avh_sent;
|
||||
static uint32_t subaru_avh_template_ts;
|
||||
static uint32_t subaru_avh_stable_since;
|
||||
static bool subaru_avh_stable;
|
||||
|
||||
static void subaru_avh_init(bool enabled) {
|
||||
subaru_avh_enabled = enabled;
|
||||
subaru_avh_done = false;
|
||||
subaru_avh_count = 0U;
|
||||
subaru_avh_start = microsecond_timer_get();
|
||||
subaru_avh_sent = 0U;
|
||||
subaru_avh_template_ts = 0U;
|
||||
subaru_avh_stable_since = 0U;
|
||||
subaru_avh_stable = false;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
subaru_avh_seen[i] = false;
|
||||
subaru_avh_seq[i] = false;
|
||||
subaru_avh_ts[i] = 0U;
|
||||
for (int j = 0; j < 8; j++) {
|
||||
subaru_avh_data[i][j] = 0U;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool subaru_avh_ready(uint32_t now) {
|
||||
bool ready = true;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
ready &= subaru_avh_seen[i] && subaru_avh_seq[i] &&
|
||||
(safety_get_ts_elapsed(now, subaru_avh_ts[i]) <= ((i == 0) ? 1500000U : 300000U));
|
||||
}
|
||||
const unsigned int rpm = ((unsigned int)subaru_avh_data[2][2] | ((unsigned int)subaru_avh_data[2][3] << 8U)) & 0x1FFFU;
|
||||
ready &= (rpm >= 400U) && (subaru_avh_data[2][4] == 0U) && (subaru_avh_data[3][3] == 4U);
|
||||
ready &= (subaru_avh_data[5][2] & 8U) != 0U;
|
||||
ready &= !vehicle_moving && !controls_allowed;
|
||||
return ready;
|
||||
}
|
||||
|
||||
static void subaru_avh_rx(const CANPacket_t *msg) {
|
||||
if (subaru_avh_enabled && !subaru_avh_done && (msg->bus == 1U)) {
|
||||
const uint32_t now = microsecond_timer_get();
|
||||
for (int i = 0; i < 6; i++) {
|
||||
if (msg->addr == SUBARU_AVH_INPUTS[i]) {
|
||||
if ((GET_LEN(msg) != 8U) || (subaru_get_checksum(msg) != subaru_compute_checksum(msg))) {
|
||||
subaru_avh_done = true;
|
||||
} else {
|
||||
const uint8_t old_counter = subaru_avh_data[i][1] & 0xFU;
|
||||
const uint8_t counter = msg->data[1] & 0xFU;
|
||||
if (!subaru_avh_seen[i] || (counter != old_counter)) {
|
||||
subaru_avh_seq[i] = subaru_avh_seen[i] && (counter == ((old_counter + 1U) & 0xFU));
|
||||
subaru_avh_seen[i] = true;
|
||||
subaru_avh_ts[i] = now;
|
||||
for (int j = 0; j < 8; j++) {
|
||||
subaru_avh_data[i][j] = msg->data[j];
|
||||
}
|
||||
}
|
||||
if (((i == 0) && ((msg->data[2] & 3U) != 0U)) ||
|
||||
((i == 1) && ((msg->data[5] & 0x20U) != 0U)) ||
|
||||
((i == 2) && (msg->data[4] != 0U)) || ((i == 3) && (msg->data[3] != 4U)) ||
|
||||
((i == 4) && (((GET_BYTES(msg, 1, 3) >> 4) & 0x1FFFU) != 0U ||
|
||||
((GET_BYTES(msg, 3, 3) >> 1) & 0x1FFFU) != 0U ||
|
||||
((GET_BYTES(msg, 4, 3) >> 6) & 0x1FFFU) != 0U ||
|
||||
((GET_BYTES(msg, 6, 2) >> 3) & 0x1FFFU) != 0U))) {
|
||||
subaru_avh_done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (controls_allowed || (safety_get_ts_elapsed(now, subaru_avh_start) > 30000000U)) {
|
||||
subaru_avh_done = true;
|
||||
}
|
||||
if (!subaru_avh_ready(now)) {
|
||||
subaru_avh_stable = false;
|
||||
if (subaru_avh_count > 0U) {
|
||||
subaru_avh_done = true;
|
||||
}
|
||||
} else if (!subaru_avh_stable) {
|
||||
subaru_avh_stable = true;
|
||||
subaru_avh_stable_since = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool subaru_avh_tx(const CANPacket_t *msg) {
|
||||
const uint32_t now = microsecond_timer_get();
|
||||
const uint32_t elapsed = safety_get_ts_elapsed(now, subaru_avh_start);
|
||||
const bool second = subaru_avh_count == 1U;
|
||||
bool allowed = subaru_avh_enabled && !subaru_avh_done && (subaru_avh_count < 2U) &&
|
||||
(msg->bus == 1U) && (GET_LEN(msg) == 8U) && !safety_rx_checks_invalid &&
|
||||
(elapsed >= 10000000U) && (elapsed <= 30000000U) && subaru_avh_ready(now) &&
|
||||
subaru_avh_stable && (safety_get_ts_elapsed(now, subaru_avh_stable_since) >= 3000000U);
|
||||
// Rejected generic RX frames may not reach our hook; invalidate their cached inputs too.
|
||||
for (int i = 0; i < current_safety_config.rx_checks_len; i++) {
|
||||
const RxCheck *check = ¤t_safety_config.rx_checks[i];
|
||||
for (int j = 0; j < 6; j++) {
|
||||
if (((unsigned int)check->msg[check->status.index].addr == SUBARU_AVH_INPUTS[j]) && (check->msg[check->status.index].bus == 1U)) {
|
||||
allowed &= check->status.valid_checksum && (check->status.wrong_counters < MAX_WRONG_COUNTERS);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (second) {
|
||||
const uint32_t spacing = safety_get_ts_elapsed(now, subaru_avh_sent);
|
||||
allowed &= (spacing >= 45000U) && (spacing <= 80000U) && (subaru_avh_ts[0] == subaru_avh_template_ts) &&
|
||||
(safety_get_ts_elapsed(now, subaru_avh_ts[0]) <= 110000U);
|
||||
} else {
|
||||
allowed &= safety_get_ts_elapsed(now, subaru_avh_ts[0]) <= 30000U;
|
||||
}
|
||||
uint8_t sum = (uint8_t)(0xBBU + 6U);
|
||||
for (int i = 1; i < 8; i++) {
|
||||
uint8_t expected = subaru_avh_data[0][i];
|
||||
if (i == 1) {
|
||||
expected = (expected & 0xF0U) | ((expected + (second ? 2U : 1U)) & 0xFU);
|
||||
} else if (i == 2) {
|
||||
expected |= 2U;
|
||||
} else {
|
||||
// Preserve every unrelated payload bit.
|
||||
}
|
||||
allowed &= msg->data[i] == expected;
|
||||
sum += expected;
|
||||
}
|
||||
allowed &= msg->data[0] == sum;
|
||||
if (allowed) {
|
||||
subaru_avh_count++;
|
||||
subaru_avh_sent = now;
|
||||
subaru_avh_template_ts = subaru_avh_ts[0];
|
||||
subaru_avh_done = second;
|
||||
}
|
||||
return allowed;
|
||||
}
|
||||
@@ -20,6 +20,7 @@ def replay_drive(msgs, safety_mode, param, alternative_experience):
|
||||
init_segment(safety, msgs, safety_mode, param)
|
||||
|
||||
rx_tot, rx_invalid, tx_tot, tx_blocked, tx_controls, tx_controls_blocked = 0, 0, 0, 0, 0, 0
|
||||
tx_lateral, tx_lateral_blocked = 0, 0
|
||||
safety_tick_rx_invalid = False
|
||||
blocked_addrs = Counter()
|
||||
invalid_addrs = set()
|
||||
@@ -38,14 +39,20 @@ def replay_drive(msgs, safety_mode, param, alternative_experience):
|
||||
if msg.which() == 'sendcan':
|
||||
for canmsg in msg.sendcan:
|
||||
_msg = package_can_msg(canmsg)
|
||||
# TX hooks can revoke permission on a violation. Count the permission
|
||||
# before checking the message, including lateral-only AOL operation.
|
||||
controls_allowed = safety.get_controls_allowed()
|
||||
lateral_allowed = controls_allowed or safety.get_aol_allowed()
|
||||
sent = safety.safety_tx_hook(_msg)
|
||||
if not sent:
|
||||
tx_blocked += 1
|
||||
tx_controls_blocked += safety.get_controls_allowed()
|
||||
tx_controls_blocked += controls_allowed
|
||||
tx_lateral_blocked += lateral_allowed
|
||||
blocked_addrs[canmsg.address] += 1
|
||||
|
||||
carlog.debug("blocked bus %d msg %d at %f" % (canmsg.src, canmsg.address, (msg.logMonoTime - start_t) / 1e9))
|
||||
tx_controls += safety.get_controls_allowed()
|
||||
tx_controls += controls_allowed
|
||||
tx_lateral += lateral_allowed
|
||||
tx_tot += 1
|
||||
elif msg.which() == 'can':
|
||||
# ignore msgs we sent
|
||||
@@ -68,9 +75,11 @@ def replay_drive(msgs, safety_mode, param, alternative_experience):
|
||||
print("total msgs with controls allowed:", tx_controls)
|
||||
print("blocked msgs:", tx_blocked)
|
||||
print("blocked with controls allowed:", tx_controls_blocked)
|
||||
print("total msgs with lateral allowed:", tx_lateral)
|
||||
print("blocked with lateral allowed:", tx_lateral_blocked)
|
||||
print("blocked addrs:", blocked_addrs)
|
||||
|
||||
return tx_controls_blocked == 0 and rx_invalid == 0 and not safety_tick_rx_invalid
|
||||
return tx_lateral_blocked == 0 and rx_invalid == 0 and not safety_tick_rx_invalid
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def replay_module(monkeypatch):
|
||||
# Load the sibling source explicitly, without native safety libraries or a
|
||||
# host-runtime snapshot. These tests isolate replay accounting, not CAN rules.
|
||||
monkeypatch.setitem(sys.modules, "opendbc.car.carlog", SimpleNamespace(carlog=Mock()))
|
||||
monkeypatch.setitem(sys.modules, "opendbc.safety.tests.libsafety", SimpleNamespace(libsafety_py=SimpleNamespace()))
|
||||
monkeypatch.setitem(sys.modules, "opendbc.safety.tests.safety_replay.helpers",
|
||||
SimpleNamespace(package_can_msg=lambda msg: msg, init_segment=Mock()))
|
||||
spec = importlib.util.spec_from_file_location("replay_drive_accounting", Path(__file__).with_name("replay_drive.py"))
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
module.tqdm = lambda msgs: msgs
|
||||
return module
|
||||
|
||||
|
||||
@pytest.mark.parametrize("controls,aol,accepted,post_controls,post_aol", [
|
||||
(False, True, False, False, True), # AOL-only denial must fail replay.
|
||||
(False, True, False, False, False), # A TX hook can revoke AOL permission.
|
||||
(True, False, False, False, False), # A TX hook can revoke controls permission.
|
||||
(False, False, False, False, False), # Expected inactive blocks remain allowed.
|
||||
(False, False, False, True, True), # Post-hook permission must not misclassify a block.
|
||||
(False, True, True, False, True),
|
||||
(True, False, True, True, False),
|
||||
(True, True, True, True, True), # Count overlapping permissions only once.
|
||||
])
|
||||
def test_tx_authorization_accounted_before_hook(replay_module, capsys, controls, aol, accepted, post_controls, post_aol):
|
||||
state = SimpleNamespace(controls=controls, aol=aol)
|
||||
|
||||
def tx_hook(msg):
|
||||
state.controls = post_controls
|
||||
state.aol = post_aol
|
||||
return accepted
|
||||
|
||||
safety = Mock()
|
||||
safety.set_safety_hooks.return_value = 0
|
||||
safety.get_controls_allowed.side_effect = lambda: state.controls
|
||||
safety.get_aol_allowed.side_effect = lambda: state.aol
|
||||
safety.safety_tx_hook.side_effect = tx_hook
|
||||
replay_module.libsafety_py.libsafety = safety
|
||||
packet = SimpleNamespace(address=0x488, src=0, dat=b"\x00" * 4)
|
||||
msg = SimpleNamespace(logMonoTime=0, sendcan=[packet], which=lambda: "sendcan")
|
||||
|
||||
result = replay_module.replay_drive([msg], 10, 0, 0)
|
||||
|
||||
lateral_allowed = controls or aol
|
||||
assert result == (accepted or not lateral_allowed)
|
||||
safety.safety_tx_hook.assert_called_once_with(packet)
|
||||
output = capsys.readouterr().out
|
||||
assert "total openpilot msgs: 1\n" in output
|
||||
assert f"total msgs with controls allowed: {int(controls)}\n" in output
|
||||
assert f"blocked msgs: {int(not accepted)}\n" in output
|
||||
assert f"blocked with controls allowed: {int(controls and not accepted)}\n" in output
|
||||
assert f"total msgs with lateral allowed: {int(lateral_allowed)}\n" in output
|
||||
assert f"blocked with lateral allowed: {int(lateral_allowed and not accepted)}\n" in output
|
||||
@@ -4,6 +4,7 @@ from opendbc.can import CANPacker
|
||||
from opendbc.car import create_gas_interceptor_command
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
from opendbc.safety.tests.test_hyundai import checksum
|
||||
|
||||
|
||||
@pytest.mark.parametrize("param", [0x9405, 0x9C05, 0x9401, 0x1005, 0])
|
||||
@@ -82,3 +83,49 @@ def test_non_ray_hyundai_ev_keeps_native_driver_gas_detection():
|
||||
native_gas = bytes.fromhex("004e008000ae0700")
|
||||
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(0x371, 0, native_gas))
|
||||
assert safety.get_gas_pressed_prev()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("controls_allowed", [False, True])
|
||||
def test_ray_native_cruise_cancel_allowed_during_pedal_override(controls_allowed):
|
||||
safety = libsafety_py.libsafety
|
||||
safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x9405)
|
||||
safety.init_tests()
|
||||
safety.set_controls_allowed(controls_allowed)
|
||||
safety.set_gas_pressed_prev(True)
|
||||
packer = CANPacker("hyundai_can_refresh_generated")
|
||||
addr, dat, bus = packer.make_can_msg("CLU11", 0, {"CF_Clu_CruiseSwState": 4})
|
||||
assert safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, dat))
|
||||
|
||||
pedal_packer = CANPacker("hyundai_kia_ray_pedal")
|
||||
addr, dat, bus = create_gas_interceptor_command(pedal_packer, 0.1, 3)
|
||||
assert not safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, dat))
|
||||
|
||||
|
||||
def test_ray_standstill_launch_obeys_hardware_brake_override():
|
||||
safety = libsafety_py.libsafety
|
||||
safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x9405)
|
||||
safety.init_tests()
|
||||
packer = CANPacker("hyundai_can_refresh_generated")
|
||||
pedal_packer = CANPacker("hyundai_kia_ray_pedal")
|
||||
|
||||
def rx(name, values):
|
||||
addr, dat, bus = checksum(packer.make_can_msg(name, 0, values))
|
||||
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, bus, dat))
|
||||
|
||||
def tx(gas):
|
||||
addr, dat, bus = create_gas_interceptor_command(pedal_packer, gas, 0)
|
||||
return safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, dat))
|
||||
|
||||
rx("WHL_SPD11", {"WHL_SPD_FL": 0, "WHL_SPD_RR": 0})
|
||||
assert not safety.get_vehicle_moving()
|
||||
rx("TCS13", {"DriverOverride": 2})
|
||||
safety.set_controls_allowed(True)
|
||||
assert safety.get_brake_pressed_prev()
|
||||
assert tx(0)
|
||||
assert not tx(0.012)
|
||||
rx("TCS13", {"DriverOverride": 0})
|
||||
assert not safety.get_brake_pressed_prev()
|
||||
assert tx(0.012)
|
||||
rx("TCS13", {"DriverOverride": 2})
|
||||
assert not tx(0.012)
|
||||
assert tx(0)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import pytest
|
||||
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car.subaru.avh import AVH_REQUEST, AVH_STATUS, INPUTS, avh_request, checksum
|
||||
from opendbc.car.subaru.tests.test_avh import sample
|
||||
from opendbc.car.subaru.values import SubaruSafetyFlags
|
||||
from opendbc.safety.tests.libsafety import libsafety_py
|
||||
|
||||
FLAGS = int(SubaruSafetyFlags.GEN2 | SubaruSafetyFlags.LKAS_ANGLE | SubaruSafetyFlags.FIXED_ANGLE_LIMITS |
|
||||
SubaruSafetyFlags.STOP_START_BUTTON | SubaruSafetyFlags.AVH_STARTUP)
|
||||
|
||||
|
||||
def packet(address, data, bus=1):
|
||||
return libsafety_py.make_CANPacket(address, bus, data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def safety():
|
||||
s = libsafety_py.libsafety
|
||||
s.set_timer(0)
|
||||
assert s.set_safety_hooks(CarParams.SafetyModel.subaru, FLAGS) == 0
|
||||
s.set_controls_allowed(False)
|
||||
for tick in range(101):
|
||||
s.set_timer(tick * 100_000)
|
||||
for address in INPUTS:
|
||||
if address != AVH_REQUEST or tick % 10 == 0:
|
||||
assert s.safety_rx_hook(packet(address, sample(address, tick // 10 if address == AVH_REQUEST else tick)))
|
||||
return s
|
||||
|
||||
|
||||
def request(step=1):
|
||||
return avh_request(sample(AVH_REQUEST, 10), step)[1]
|
||||
|
||||
|
||||
def test_pair_and_third_frame_blocked(safety):
|
||||
assert safety.safety_tx_hook(packet(AVH_REQUEST, request()))
|
||||
safety.set_timer(10_050_000)
|
||||
assert safety.safety_tx_hook(packet(AVH_REQUEST, request(2)))
|
||||
safety.set_timer(10_100_000)
|
||||
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request(2)))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('byte', range(8))
|
||||
def test_payload_mutation_blocked(safety, byte):
|
||||
data = bytearray(request())
|
||||
data[byte] ^= 4
|
||||
if byte:
|
||||
data[0] = checksum(AVH_REQUEST, data)
|
||||
assert not safety.safety_tx_hook(packet(AVH_REQUEST, data))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('bus', [0, 2])
|
||||
def test_wrong_bus_blocked(safety, bus):
|
||||
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request(), bus))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('delay', [44_999, 80_001])
|
||||
def test_followup_timing(safety, delay):
|
||||
assert safety.safety_tx_hook(packet(AVH_REQUEST, request()))
|
||||
safety.set_timer(10_000_000 + delay)
|
||||
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request(2)))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('address,offset,value', [(AVH_REQUEST, 2, 1), (AVH_STATUS, 5, 32),
|
||||
(0x40, 4, 1), (0x48, 3, 3), (0x13A, 2, 1)])
|
||||
def test_abort_on_manual_ack_or_movement(safety, address, offset, value):
|
||||
assert safety.safety_tx_hook(packet(AVH_REQUEST, request()))
|
||||
safety.set_timer(10_050_000)
|
||||
data = bytearray(sample(address, 11 if address == AVH_REQUEST else 101))
|
||||
data[offset] = value
|
||||
data[0] = checksum(address, data)
|
||||
assert safety.safety_rx_hook(packet(address, data))
|
||||
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request(2)))
|
||||
|
||||
|
||||
def test_stale_template_and_status_tx_blocked(safety):
|
||||
assert not safety.safety_tx_hook(packet(AVH_STATUS, sample(AVH_STATUS, 1)))
|
||||
safety.set_timer(10_030_001)
|
||||
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request()))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('flags', [FLAGS & ~1024, FLAGS | 32, FLAGS | 2, FLAGS & ~16, FLAGS | 512])
|
||||
def test_permission_gates(safety, flags):
|
||||
assert safety.set_safety_hooks(CarParams.SafetyModel.subaru, flags) == 0
|
||||
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request()))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('reason', ['new_template', 'corrupt', 'engaged', 'expired', 'duplicate'])
|
||||
def test_extra_failure_gates(safety, reason):
|
||||
if reason == 'engaged':
|
||||
safety.set_controls_allowed(True)
|
||||
elif reason == 'expired':
|
||||
safety.set_timer(30_000_001)
|
||||
elif reason == 'duplicate':
|
||||
safety.set_timer(10_040_000)
|
||||
assert safety.safety_rx_hook(packet(AVH_REQUEST, sample(AVH_REQUEST, 10)))
|
||||
elif reason == 'corrupt':
|
||||
data = bytearray(sample(AVH_REQUEST, 11))
|
||||
data[0] ^= 1
|
||||
safety.safety_rx_hook(packet(AVH_REQUEST, data))
|
||||
else:
|
||||
assert safety.safety_tx_hook(packet(AVH_REQUEST, request()))
|
||||
safety.set_timer(10_050_000)
|
||||
assert safety.safety_rx_hook(packet(AVH_REQUEST, sample(AVH_REQUEST, 11)))
|
||||
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request(2)))
|
||||
return
|
||||
assert not safety.safety_tx_hook(packet(AVH_REQUEST, request()))
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,2 +1,2 @@
|
||||
extern const uint8_t gitversion[19];
|
||||
const uint8_t gitversion[19] = "DEV-14370fe9-DEBUG";
|
||||
const uint8_t gitversion[19] = "DEV-5925aecd-DEBUG";
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user