mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a9a80d34e7 | |||
| 3093e5b799 | |||
| f633c05b32 | |||
| 1d30dc3434 | |||
| e1e3be94fe | |||
| 31695937b3 | |||
| 7056dcb1fa |
@@ -1,59 +0,0 @@
|
||||
name: HKG angle recovery qualification
|
||||
on:
|
||||
push:
|
||||
branches: [codex/hkg-angle-retention-20261106]
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
recovery:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
SCALE: '1'
|
||||
RAYLIB_BACKEND: headless
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Fetch exact baseline source
|
||||
run: git -c gc.auto=0 -c maintenance.auto=false fetch --no-tags --depth=1 --filter=blob:none origin 68434cf39c501604fe962e9488a03bde0fed0d73
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: '1.25.1'
|
||||
cache-dependency-path: mapd_repo/go.sum
|
||||
- name: Fetch pinned map provider dependencies
|
||||
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
|
||||
- name: Run steering retention and independent safety veto regressions
|
||||
run: |
|
||||
.venv/bin/python -m pytest -q --junitxml=qualification.xml openpilot/starpilot/aol/tests/test_hkg_angle_eps_retention.py openpilot/starpilot/aol/tests/test_pe_combined_profile_contract.py openpilot/starpilot/aol/tests/test_stock_eps_recovery.py openpilot/starpilot/aol/tests/test_runtime.py openpilot/starpilot/aol/tests/test_source_pause_driver_monitoring.py openpilot/system/hardware/tests openpilot/starpilot/drive_state/tests/test_hardware_lifecycle.py openpilot/starpilot/ui/tests/test_weather.py openpilot/starpilot/flm/tests/test_pid_global_capture.py openpilot/starpilot/flm/tests/test_registered_controls_global.py::test_actual_controls_source_freshness_calibration_and_override_rejection openpilot/starpilot/lateral/tests/test_ev6_default.py openpilot/starpilot/lateral/tests/test_ioniq6_policy.py tools/ci/tests/test_replay_references.py
|
||||
- name: Check owned Python files
|
||||
run: |
|
||||
.venv/bin/ruff check openpilot/starpilot/aol/temporary_steering.py openpilot/starpilot/aol/tests/test_hkg_angle_eps_retention.py openpilot/starpilot/aol/transport_pause.py openpilot/system/hardware/hardwared.py openpilot/system/hardware/hardware_timing.py openpilot/system/hardware/tests/test_hardware_timing.py openpilot/starpilot/ui/tests/test_weather.py openpilot/starpilot/lateral/tests/test_ev6_default.py openpilot/starpilot/lateral/tests/test_ioniq6_policy.py
|
||||
.venv/bin/ty check openpilot/starpilot/aol/temporary_steering.py openpilot/starpilot/aol/tests/test_hkg_angle_eps_retention.py openpilot/starpilot/aol/transport_pause.py openpilot/system/hardware/hardwared.py openpilot/system/hardware/hardware_timing.py openpilot/system/hardware/tests/test_hardware_timing.py openpilot/starpilot/ui/tests/test_weather.py openpilot/starpilot/lateral/tests/test_ev6_default.py openpilot/starpilot/lateral/tests/test_ioniq6_policy.py
|
||||
- name: Compare both reviewed default-controller recordings
|
||||
run: .venv/bin/python openpilot/selfdrive/test/process_replay/test_processes.py --whitelist-cars HYUNDAI HYUNDAI2 --whitelist-procs controlsd --output reviewed-defaults -j2
|
||||
- name: Save exact results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: hkg-angle-recovery-results
|
||||
path: |
|
||||
baseline.xml
|
||||
baseline-result.json
|
||||
qualification.xml
|
||||
reviewed-defaults/coverage.json
|
||||
reviewed-defaults/diff.txt
|
||||
@@ -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;
|
||||
|
||||
@@ -280,6 +280,65 @@ static bool volvo_cma_receiver_quality(const CANPacket_t *msg) {
|
||||
return volvo_cma_quality(msg) && volvo_cma_lca2_quality(msg);
|
||||
}
|
||||
|
||||
// Unchanged native status is transport evidence, never valid control evidence.
|
||||
static uint8_t volvo_cma_pscm_status_data[4][8];
|
||||
static uint32_t volvo_cma_pscm_status_ts[4];
|
||||
static bool volvo_cma_pscm_status_valid[4];
|
||||
static uint8_t volvo_cma_pscm_status_index;
|
||||
static uint8_t volvo_cma_pscm_status_nibble;
|
||||
static bool volvo_cma_pscm_status_checksum_valid;
|
||||
static bool volvo_cma_pscm_status_quality_valid;
|
||||
|
||||
static void volvo_cma_clear_pscm_status(void) {
|
||||
for (uint8_t i = 0U; i < 4U; i++) {
|
||||
volvo_cma_pscm_status_valid[i] = false;
|
||||
}
|
||||
volvo_cma_pscm_status_index = 0U;
|
||||
}
|
||||
|
||||
static bool volvo_cma_receiver_quality_rx(const CANPacket_t *msg) {
|
||||
const bool quality_valid = volvo_cma_receiver_quality(msg);
|
||||
if (volvo_cma_lca2_mod15 && msg_matches(msg, 0x17U, 2U, 8U) &&
|
||||
!msg->returned && !msg->rejected && !msg->extended && !msg->fd) {
|
||||
const uint8_t nibble = msg->data[2] >> 4U;
|
||||
const bool checksum_valid = volvo_cma_get_checksum(msg) == volvo_cma_compute_checksum(msg);
|
||||
// Retain the existing four-frame transport tolerance, but never replay an
|
||||
// earlier status/validation class after a newly observed native transition.
|
||||
if ((nibble != volvo_cma_pscm_status_nibble) ||
|
||||
(checksum_valid != volvo_cma_pscm_status_checksum_valid) ||
|
||||
(quality_valid != volvo_cma_pscm_status_quality_valid)) {
|
||||
volvo_cma_clear_pscm_status();
|
||||
}
|
||||
volvo_cma_pscm_status_nibble = nibble;
|
||||
volvo_cma_pscm_status_checksum_valid = checksum_valid;
|
||||
volvo_cma_pscm_status_quality_valid = quality_valid;
|
||||
const uint8_t slot = volvo_cma_pscm_status_index;
|
||||
for (uint8_t i = 0U; i < 8U; i++) {
|
||||
volvo_cma_pscm_status_data[slot][i] = msg->data[i];
|
||||
}
|
||||
volvo_cma_pscm_status_ts[slot] = microsecond_timer_get();
|
||||
volvo_cma_pscm_status_valid[slot] = true;
|
||||
volvo_cma_pscm_status_index = (slot + 1U) % 4U;
|
||||
}
|
||||
return quality_valid;
|
||||
}
|
||||
|
||||
static bool volvo_cma_exact_pscm_status(const CANPacket_t *msg) {
|
||||
bool matches = false;
|
||||
if (msg_matches(msg, 0x17U, 0U, 8U) &&
|
||||
!msg->returned && !msg->rejected && !msg->extended && !msg->fd) {
|
||||
for (uint8_t slot = 0U; slot < 4U; slot++) {
|
||||
bool same = volvo_cma_pscm_status_valid[slot] &&
|
||||
(safety_get_ts_elapsed(microsecond_timer_get(), volvo_cma_pscm_status_ts[slot]) <= 100000U);
|
||||
for (uint8_t i = 0U; i < 8U; i++) {
|
||||
same &= msg->data[i] == volvo_cma_pscm_status_data[slot][i];
|
||||
}
|
||||
matches |= same;
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
// The Polestar source clocks advance nominally2/4 per50Hz frame, with
|
||||
// observed sampling jitter of1..3/3..5. Frozen/jumped counters retain the shared tolerance.
|
||||
static uint8_t volvo_cma_lca2_counter(const CANPacket_t *msg) {
|
||||
@@ -383,7 +442,8 @@ static bool volvo_cma_tx_hook(const CANPacket_t *msg) {
|
||||
// policy or intent and retains the existing health deadlines/tolerances.
|
||||
bool tx;
|
||||
if (!controls_allowed || !volvo_cma_drive_current() || !aol_rx_healthy()) {
|
||||
tx = volvo_cma_exact_stock(msg);
|
||||
tx = (volvo_cma_lca2_mod15 && (msg->addr == 0x17U)) ?
|
||||
volvo_cma_exact_pscm_status(msg) : volvo_cma_exact_stock(msg);
|
||||
} else {
|
||||
tx = volvo_cma_receiver_quality(msg) && volvo_cma_source_projection(msg);
|
||||
if ((msg->addr == 0x69U) || (msg->addr == 0x67U) || (msg->addr == 0x17U)) {
|
||||
@@ -514,6 +574,10 @@ static RxCheck *volvo_cma_rx_checks_workspace(void) {
|
||||
static safety_config volvo_cma_init(uint16_t param) {
|
||||
RxCheck *const rx_workspace = volvo_cma_rx_checks_workspace();
|
||||
volvo_cma_lca2_mod15 = param == 5U;
|
||||
volvo_cma_clear_pscm_status();
|
||||
volvo_cma_pscm_status_nibble = 0U;
|
||||
volvo_cma_pscm_status_checksum_valid = false;
|
||||
volvo_cma_pscm_status_quality_valid = false;
|
||||
volvo_cma_lca2_second_seen = false;
|
||||
volvo_cma_lca2_second_last = 0U;
|
||||
volvo_cma_lca2_second_wrong = 0;
|
||||
@@ -635,6 +699,6 @@ const safety_hooks volvo_cma_hooks = {
|
||||
.get_counter = volvo_cma_lca2_counter,
|
||||
.get_checksum = volvo_cma_get_checksum,
|
||||
.compute_checksum = volvo_cma_compute_checksum,
|
||||
.get_quality_flag_valid = volvo_cma_receiver_quality,
|
||||
.get_quality_flag_valid = volvo_cma_receiver_quality_rx,
|
||||
// No custom fwd hook - stock LCA always blocked by .check_relay = true
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ static uint32_t volvo_dispatch_compute_checksum(const CANPacket_t *msg) {
|
||||
return volvo_dispatch_c1 ? 0U : volvo_cma_compute_checksum(msg);
|
||||
}
|
||||
static bool volvo_dispatch_quality(const CANPacket_t *msg) {
|
||||
return volvo_dispatch_c1 || volvo_cma_receiver_quality(msg);
|
||||
return volvo_dispatch_c1 || volvo_cma_receiver_quality_rx(msg);
|
||||
}
|
||||
const safety_hooks volvo_dispatch_hooks = {.init = volvo_dispatch_init, .rx = volvo_dispatch_rx, .tx = volvo_dispatch_tx,
|
||||
.get_counter = volvo_dispatch_get_counter,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -86,3 +86,199 @@ class TestPolestarCounters(unittest.TestCase):
|
||||
self.assertTrue(self.safety.safety_config_valid())
|
||||
self.assertTrue(self.safety.get_controls_allowed())
|
||||
self.assertTrue(self.tx(self.lca_command()))
|
||||
|
||||
def unknown_pscm_status(self):
|
||||
return 0x17, bytes.fromhex('d400a0858000ffff'), 2
|
||||
|
||||
def test_unknown_status_relays_without_becoming_control_evidence(self):
|
||||
for cruise in (False, True):
|
||||
with self.subTest(cruise=cruise):
|
||||
self.reset()
|
||||
incoming = self.feed(cruise=cruise)
|
||||
status = self.unknown_pscm_status()
|
||||
self.assertFalse(self.rx(status))
|
||||
self.assertFalse(self.safety.get_controls_allowed())
|
||||
self.safety.safety_tick()
|
||||
self.assertFalse(self.safety.safety_config_valid())
|
||||
self.assertTrue(self.tx(self.relay(status)))
|
||||
self.assertFalse(self.tx(self.relay(incoming[0x17])))
|
||||
self.assertFalse(self.tx(self.lca_command()))
|
||||
self.safety.set_controls_allowed(True)
|
||||
self.assertTrue(self.tx(self.relay(status)))
|
||||
self.assertFalse(self.tx(self.lca_command()))
|
||||
for address in cma.RELAY_BUSES:
|
||||
if address != 0x17:
|
||||
self.assertTrue(self.tx(self.relay(incoming[address])))
|
||||
self.assertEqual(self.safety.safety_fwd_hook(2, 0x17), -1)
|
||||
|
||||
def test_unknown_status_requires_exact_native_bytes(self):
|
||||
self.feed()
|
||||
status = self.unknown_pscm_status()
|
||||
self.assertFalse(self.rx(status))
|
||||
for bit in range(64):
|
||||
data = bytearray(status[1])
|
||||
data[bit // 8] ^= 1 << (bit % 8)
|
||||
self.assertFalse(self.tx((0x17, data, 0)), bit)
|
||||
self.assertTrue(self.tx(self.relay(status)))
|
||||
|
||||
def test_unknown_status_source_identity_cannot_be_forged(self):
|
||||
status = self.unknown_pscm_status()
|
||||
for field in ('returned', 'rejected', 'extended', 'fd'):
|
||||
with self.subTest(field=field):
|
||||
self.reset()
|
||||
self.feed(cruise=False)
|
||||
packet = cma.libsafety_py.make_CANPacket(status[0], 2, status[1])
|
||||
setattr(packet[0], field, 1)
|
||||
self.safety.safety_rx_hook(packet)
|
||||
self.assertFalse(self.tx(self.relay(status)))
|
||||
for bus in (0, 1, 3, 4, 5, 6, 7):
|
||||
self.reset()
|
||||
self.feed(cruise=False)
|
||||
self.rx((0x17, status[1], bus))
|
||||
self.assertFalse(self.tx(self.relay(status)))
|
||||
for length in (*range(8), 12):
|
||||
self.reset()
|
||||
self.feed(cruise=False)
|
||||
self.rx((0x17, (status[1] + bytes(4))[:length], 2))
|
||||
self.assertFalse(self.tx(self.relay(status)))
|
||||
|
||||
def test_unknown_status_tx_cannot_seed_or_refresh_source(self):
|
||||
self.feed(cruise=False)
|
||||
status = self.unknown_pscm_status()
|
||||
self.assertFalse(self.tx(self.relay(status)))
|
||||
self.assertFalse(self.rx(status))
|
||||
self.safety.set_timer(110000)
|
||||
self.assertTrue(self.tx(self.relay(status)))
|
||||
self.safety.set_timer(110001)
|
||||
self.assertFalse(self.tx(self.relay(status)))
|
||||
self.assertFalse(self.tx(self.relay(status)))
|
||||
|
||||
def test_unknown_status_original_age_survives_timer_wrap(self):
|
||||
self.feed(cruise=False)
|
||||
start = 0xFFFFFF00
|
||||
self.safety.set_timer(start)
|
||||
status = self.unknown_pscm_status()
|
||||
self.assertFalse(self.rx(status))
|
||||
for age, accepted in ((90000, True), (100000, True), (100001, False)):
|
||||
self.safety.set_timer((start + age) & 0xFFFFFFFF)
|
||||
self.assertEqual(bool(self.tx(self.relay(status))), accepted)
|
||||
|
||||
def test_status_preserves_four_frame_transport_tolerance(self):
|
||||
for unknown in (False, True):
|
||||
with self.subTest(unknown=unknown):
|
||||
self.reset()
|
||||
incoming = self.feed(cruise=False)
|
||||
source = self.unknown_pscm_status() if unknown else incoming[0x17]
|
||||
frames = [(0x17, source[1][:-1] + bytes([i]), 2) for i in range(5)]
|
||||
for i, frame in enumerate(frames):
|
||||
self.safety.set_timer(20000 + i * 10000)
|
||||
self.assertEqual(bool(self.rx(frame)), not unknown)
|
||||
for retained in frames[max(0, i - 3):i + 1]:
|
||||
self.assertTrue(self.tx(self.relay(retained)))
|
||||
self.assertFalse(self.tx(self.relay(frames[0])))
|
||||
# A new native arrival never refreshes another slot's original age.
|
||||
self.safety.set_timer(130000)
|
||||
self.assertTrue(self.tx(self.relay(frames[1])))
|
||||
self.safety.set_timer(130001)
|
||||
self.assertFalse(self.tx(self.relay(frames[1])))
|
||||
self.assertTrue(self.tx(self.relay(frames[2])))
|
||||
|
||||
def test_native_status_transition_flushes_old_transport_evidence(self):
|
||||
incoming = self.feed(cruise=False)
|
||||
healthy = incoming[0x17]
|
||||
first = self.unknown_pscm_status()
|
||||
second = first[0], first[1][:-1] + b'\xfe', first[2]
|
||||
restored = healthy[0], healthy[1][:-1] + b'\x01', healthy[2]
|
||||
self.assertFalse(self.rx(first))
|
||||
self.assertTrue(self.tx(self.relay(first)))
|
||||
self.assertFalse(self.tx(self.relay(healthy)))
|
||||
self.assertFalse(self.rx(second))
|
||||
self.assertTrue(self.tx(self.relay(first)))
|
||||
self.assertTrue(self.tx(self.relay(second)))
|
||||
self.assertTrue(self.rx(restored))
|
||||
for stale in (healthy, first, second):
|
||||
self.assertFalse(self.tx(self.relay(stale)))
|
||||
self.assertTrue(self.tx(self.relay(restored)))
|
||||
self.assertFalse(self.safety.get_controls_allowed())
|
||||
|
||||
def test_each_native_validation_class_change_flushes_transport_evidence(self):
|
||||
healthy = self.feed(cruise=False)[0x17]
|
||||
# Keep the high nibble at 8 and change one validation boolean at a time:
|
||||
# (checksum, quality): TT -> FT -> FF -> TF -> TT.
|
||||
bad_checksum = bytearray(healthy[1])
|
||||
bad_checksum[0] ^= 1
|
||||
bad_both = bytearray(bad_checksum)
|
||||
bad_both[1] |= 8 # This quality bit is unconstrained by the checksum.
|
||||
bad_quality = bytearray(healthy[1])
|
||||
bad_quality[1] |= 8
|
||||
frames = [healthy, (0x17, bad_checksum, 2), (0x17, bad_both, 2),
|
||||
(0x17, bad_quality, 2), (0x17, healthy[1][:-1] + b'\x01', 2)]
|
||||
for i, frame in enumerate(frames):
|
||||
self.assertEqual(bool(self.rx(frame)), i in (0, 4))
|
||||
self.assertTrue(self.tx(self.relay(frame)))
|
||||
for stale in frames[:i]:
|
||||
self.assertFalse(self.tx(self.relay(stale)))
|
||||
self.assertFalse(self.safety.get_controls_allowed())
|
||||
|
||||
def test_unknown_status_cache_does_not_cross_mode_reset(self):
|
||||
self.feed(cruise=False)
|
||||
status = self.unknown_pscm_status()
|
||||
self.assertFalse(self.rx(status))
|
||||
self.assertTrue(self.tx(self.relay(status)))
|
||||
self.reset()
|
||||
self.assertFalse(self.tx(self.relay(status)))
|
||||
self.feed(cruise=False)
|
||||
self.assertFalse(self.tx(self.relay(status)))
|
||||
|
||||
def test_unknown_status_does_not_expand_other_profiles(self):
|
||||
status = self.unknown_pscm_status()
|
||||
for profile in (0, 1):
|
||||
self.assertEqual(self.safety.set_safety_hooks(36, profile), 0)
|
||||
self.safety.init_tests()
|
||||
self.safety.set_timer(10000)
|
||||
self.assertFalse(self.rx(status))
|
||||
self.assertFalse(self.tx(self.relay(status)))
|
||||
self.assertNotEqual(self.safety.set_safety_hooks(36, 4), 0)
|
||||
|
||||
def test_unknown_status_remains_blocked_on_relay_malfunction(self):
|
||||
self.feed(cruise=False)
|
||||
status = self.unknown_pscm_status()
|
||||
self.assertFalse(self.rx(status))
|
||||
self.assertTrue(self.tx(self.relay(status)))
|
||||
self.rx((0x58, bytes(8), 2))
|
||||
self.assertTrue(self.safety.get_relay_malfunction())
|
||||
self.assertFalse(self.tx(self.relay(status)))
|
||||
|
||||
def test_unknown_status_tx_identity_matches_native_framing(self):
|
||||
self.feed(cruise=False)
|
||||
status = self.unknown_pscm_status()
|
||||
self.assertFalse(self.rx(status))
|
||||
for field in ('returned', 'rejected', 'extended', 'fd'):
|
||||
packet = cma.libsafety_py.make_CANPacket(0x17, 0, status[1])
|
||||
setattr(packet[0], field, 1)
|
||||
self.assertFalse(self.safety.safety_tx_hook(packet), field)
|
||||
self.assertTrue(self.tx(self.relay(status)))
|
||||
for bus in range(1, 8):
|
||||
self.assertFalse(self.tx((0x17, status[1], bus)))
|
||||
for length in (*range(8), 12):
|
||||
self.assertFalse(self.tx((0x17, (status[1] + bytes(4))[:length], 0)))
|
||||
self.assertFalse(self.tx((0x16, status[1], 0)))
|
||||
self.assertTrue(self.tx(self.relay(status)))
|
||||
|
||||
def test_modified_status_is_rejected_until_controller_returns_to_stock(self):
|
||||
self.feed()
|
||||
status = self.unknown_pscm_status()
|
||||
stock = {
|
||||
'CHECKSUM_1': 0xD4, 'LCA_ENABLED_ECHO': 0, 'SIG1_BYTE_1_HI_NIBBLE': 0,
|
||||
'SIG1_REPLICA_BYTE_2_LO_NIBLE': 0, 'NEW_SIGNAL_2': 10, 'CHECKSUM_2': 0x85,
|
||||
'BYTE_4_5': 0x8000, 'BYTE_6': 0xFF, 'BYTE_7': 0xFF,
|
||||
}
|
||||
self.assertFalse(self.rx(status))
|
||||
self.assertFalse(self.safety.get_controls_allowed())
|
||||
modified = cma.cma_can.create_pscm_related_message(self.mid, True, False, stock, 1)
|
||||
self.assertNotEqual(modified[1], status[1])
|
||||
self.assertFalse(self.tx(modified))
|
||||
unchanged = cma.cma_can.create_pscm_related_message(self.mid, False, False, stock, 1)
|
||||
self.assertEqual(unchanged[1], status[1])
|
||||
self.assertTrue(self.tx(unchanged))
|
||||
self.assertFalse(self.safety.get_controls_allowed())
|
||||
|
||||
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,4 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
extern const uint8_t gitversion[29];
|
||||
const uint8_t gitversion[29] = "DEV-5d6e6afc-Pf38f504f-DEBUG";
|
||||
const uint8_t gitversion[29] = "DEV-3093e5b7-Pf38f504f-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.
@@ -1 +1 @@
|
||||
DEV-5d6e6afc-Pf38f504f-DEBUG
|
||||
DEV-3093e5b7-Pf38f504f-DEBUG
|
||||
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
{"protected_inventory_sha256": "b1b32d57b53f664878ae381bd92018b4d4b6b57d6d7f92e465c5ed15aaa09728", "schema_version": 1}
|
||||
{"protected_inventory_sha256": "f1741daeb7b18776c77b38c839c9a6e97bfb97229851365246afdf6e28936f60", "schema_version": 1}
|
||||
|
||||
Reference in New Issue
Block a user