Compare commits

...

7 Commits

Author SHA1 Message Date
firestar5683 a9a80d34e7 Build matching Polestar status relay firmware 2026-11-07 18:02:56 -06:00
firestar5683 3093e5b799 Preserve Polestar stock status relay 2026-11-07 17:58:51 -06:00
firestar5683 f633c05b32 Gate Ioniq 6 blind-spot warnings on queued lane changes 2026-11-07 17:33:23 -06:00
firestar5683 1d30dc3434 Align HKG default-controller regression expectations 2026-11-06 22:18:55 -06:00
firestar5683 e1e3be94fe Enable driving in learning regression fixtures 2026-11-06 21:46:34 -06:00
firestar5683 31695937b3 Fix inherited static-analysis failures 2026-11-06 21:35:36 -06:00
firestar5683 7056dcb1fa Preserve HKG angle arming through temporary refusals 2026-11-06 21:23:09 -06:00
59 changed files with 2244 additions and 1678 deletions
@@ -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;
+66 -2
View File
@@ -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())
File diff suppressed because it is too large Load Diff
@@ -49,7 +49,8 @@ def temporary_steering_intent_current(self, sm, cp, cs, axis, stamp, now_ns, *,
now_ns - previous[8] <= AXIS_MAX_AGE_NS and
0 < self.lateral_source_ns <= now_ns and
now_ns - self.lateral_source_ns <= AXIS_MAX_AGE_NS)
held_original = bool(axis.desiredLateral and axis.lateralActive and axis.nativeAcknowledged and
held_original = bool(axis.desiredLateral and (axis.lateralActive or
cp.brand == 'hyundai' and str(cp.steerControlType) == 'angle' and fresh_active_successor) and axis.nativeAcknowledged and
(signature == previous or fresh_active_successor))
if not withdrawn and not held_original:
return False
@@ -150,7 +151,7 @@ def temporary_steering_intent_current(self, sm, cp, cs, axis, stamp, now_ns, *,
if (native is None or not native.pandaInventory or
stock_eps_user and (native.requestedLongitudinal or native.longitudinalAllowed) or
held_original and not (native.requestedLateral and (native.lateralAllowed or
restricted_eps and (ford_alpha_eps or not native.requestedLongitudinal and not native.longitudinalAllowed))) or
restricted_eps and (ford_alpha_eps or hkg_angle_eps or not native.requestedLongitudinal and not native.longitudinalAllowed))) or
axis.longitudinalActive and not native.longitudinalAllowed or
native.observedMonoTime < self.lateral_native_ns or
(native.pandaSerial, tuple(p.hardwareSerial for p in native.pandaInventory)) != self.lateral_inventory):
@@ -121,3 +121,103 @@ def test_angle_eps_retention_preserves_independent_vetoes(fault):
sm.update_msgs(now / 1e9, frames)
rejected = feedback.observe(sm, sd.CP, state, now, latched=True, intent_producer_session='authority-card')
assert rejected == (tick == 3)
@pytest.mark.parametrize('identity', ('pe_long', 'ev9_long'))
@pytest.mark.parametrize('full', (False, True))
@pytest.mark.parametrize('steering', (False, True))
@pytest.mark.parametrize('delivery', ('native_first', 'event_first', 'together', 'native_pause_first'))
def test_angle_eps_split_publications_preserve_only_armed_intent(identity, full, steering, delivery):
case = fixture.IoniqAvailabilityTests(methodName='runTest')
with ExitStack() as cleanup:
cleanup.callback(case.doCleanups)
cleanup.enter_context(OpenpilotPrefix())
cp = profile(identity)
assert cp is not None
sd, _, drive = case._authority_callers(full, cp=cp, genuine_events=True)
feedback = TransportPauseFeedback(sd.CP)
sm = messaging.SubMaster(['aolAxisState', 'onroadEvents', 'aolSafetyWire', 'pandaStates'])
state = fixture.car_state()
state.vEgoRaw = state.vEgo
state.cruiseState.enabled = full and cp.pcmCruise
for tick in range(9):
fault = 3 <= tick < 8
state.steerFaultTemporary = fault and not (tick == 3 and delivery == 'native_pause_first')
axis, _, command = drive(tick, observed_state=state, steering=steering if fault or delivery == 'native_pause_first' else False,
gas=delivery == 'native_pause_first',
allowed=not fault, native_request_lat=True, native_request_long=full)
now = 10_000_000_000 + tick * 10_000_000
frames = [sd.pm.messages['aolAxisState'], sd.pm.messages['onroadEvents']]
for name in ('aolSafetyWire', 'pandaStates'):
message = messaging.new_message(name, len(sd.sm[name]), valid=sd.sm.valid[name],
logMonoTime=sd.sm.logMonoTime[name])
setattr(message, name, sd.sm[name])
frames.append(message.as_reader())
assert bool(axis.lateralActive and command.latActive) == (not fault)
if tick == 3 and delivery == 'native_pause_first':
assert axis.desiredLateral and not axis.lateralActive and axis.nativeAcknowledged
state.steerFaultTemporary = True
if tick == 3 and delivery not in ('together', 'native_pause_first'):
sm.update_msgs(now / 1e9, frames[2:] if delivery == 'native_first' else frames[:2])
assert not feedback.observe(sm, sd.CP, state, now, latched=True,
intent_producer_session='authority-card'), 'NATIVE_FIRST_EPS_DISARMED'
assert not feedback.failed
joined = (frames[:2] if delivery == 'native_first' else frames[2:]) if tick == 3 and delivery in ('native_first', 'event_first') else frames
sm.update_msgs(now / 1e9, joined)
assert not feedback.observe(sm, sd.CP, state, now, latched=True,
intent_producer_session='authority-card'), 'NATIVE_PAUSE_EPS_DISARMED' if delivery == 'native_pause_first' else 'EPS_JOIN_DISARMED'
assert not feedback.failed
if fault:
assert not axis.lateralActive and not command.latActive
@pytest.mark.parametrize('identity', ('pe_long', 'ev9_long'))
@pytest.mark.parametrize('native_long_pending', (False, True))
def test_stopped_full_request_keeps_existing_lateral_intent_without_granting_commands(identity, native_long_pending):
from opendbc.car import structs
case = fixture.IoniqAvailabilityTests(methodName='runTest')
with ExitStack() as cleanup:
cleanup.callback(case.doCleanups)
cleanup.enter_context(OpenpilotPrefix())
cp = profile(identity)
assert cp is not None
sd, _, drive = case._authority_callers(False, cp=cp, genuine_events=True)
feedback = TransportPauseFeedback(sd.CP)
sm = messaging.SubMaster(['aolAxisState', 'onroadEvents', 'aolSafetyWire', 'pandaStates'])
state = fixture.car_state()
state.vEgoRaw = state.vEgo
for tick in range(7):
state.standstill = tick in (3, 4, 5)
state.vEgo = state.vEgoRaw = 0. if state.standstill else 5.
state.buttonEnable = tick == 4
state.buttonEvents = ([structs.CarState.ButtonEvent(type='accelCruise', pressed=False)] if tick == 4 else [])
requested_long = tick >= 5 or tick == 4 and native_long_pending
allowed_long = tick >= 5
state.cruiseState.enabled = allowed_long
state.cruiseState.standstill = allowed_long and state.standstill
axis, _, command = drive(tick, observed_state=state, native_request_lat=True,
allowed=not state.standstill, native_request_long=requested_long, long_allowed=allowed_long,
panda_controls_allowed=allowed_long)
now = 10_000_000_000 + tick * 10_000_000
frames = [sd.pm.messages['aolAxisState'], sd.pm.messages['onroadEvents']]
for name in ('aolSafetyWire', 'pandaStates'):
message = messaging.new_message(name, len(sd.sm[name]), valid=sd.sm.valid[name],
logMonoTime=sd.sm.logMonoTime[name])
setattr(message, name, sd.sm[name])
frames.append(message.as_reader())
sm.update_msgs(now / 1e9, frames)
if tick == 4:
assert axis.desiredLongitudinal and not axis.longitudinalActive
assert not axis.lateralActive and not command.latActive and not command.longActive
if tick == 5:
assert axis.longitudinalActive and not axis.lateralActive and not command.latActive
assert all(not p.controlsAllowed for p in sm['pandaStates'])
veto = messaging.SubMaster(['aolAxisState', 'onroadEvents', 'aolSafetyWire', 'pandaStates'])
latest = messaging.new_message('pandaStates', len(sm['pandaStates']), valid=True, logMonoTime=now)
latest.pandaStates = sm['pandaStates']
veto.update_msgs(now / 1e9, [*frames[:3], latest.as_reader()])
assert not feedback._angle_stop_intent_current(veto, sd.CP, state, axis, now, now,
latched=True, producer='authority-card')
assert not feedback.observe(sm, sd.CP, state, now, latched=True,
intent_producer_session='authority-card'), f'STOPPED_FULL_REQUEST_DISARMED tick={tick}'
assert not feedback.failed
+13 -3
View File
@@ -1177,6 +1177,7 @@ class TransportPauseFeedback:
ccnc_stop = bool((cs.standstill or 0 <= cs.vEgoRaw <= .3) and cp.openpilotLongitudinalControl and
cp.carFingerprint in (CAR.KIA_EV9, CAR.HYUNDAI_IONIQ_5_PE) and not cs.accFaulted and not cs.steeringDisengage)
combined_stop = bool(ccnc_stop and axis.desiredLongitudinal and axis.longitudinalActive)
pending_stop = bool(ccnc_stop and axis.desiredLongitudinal and not axis.longitudinalActive and not axis.lateralActive)
subaru_stop = cs.standstill and subaru_restriction(cp, cs)
if (not (qualified(cp, marked_only=True) or mazda_qualified(cp, marked_only=True) or subaru_stop or ccnc_stop) or
cs.gearShifter != car.CarState.GearShifter.drive or
@@ -1187,7 +1188,7 @@ class TransportPauseFeedback:
not producer or producer != self.availability_producer or
axis.intentProducerSessionId != producer or axis.intentIntegrityEpoch != self.availability_epoch or
not (axis.desiredLateral or subaru_stop and not axis.lateralActive) or not axis.nativeAcknowledged or
(axis.desiredLongitudinal or axis.longitudinalActive) and not combined_stop or
(axis.desiredLongitudinal or axis.longitudinalActive) and not (combined_stop or pending_stop) or
not 0 < axis.sourceCarStateMonoTime <= stamp <= now_ns or
now_ns - axis.sourceCarStateMonoTime > AXIS_MAX_AGE_NS):
return False
@@ -1198,10 +1199,19 @@ class TransportPauseFeedback:
event.name not in (log.OnroadEvent.EventName.pcmDisable, log.OnroadEvent.EventName.pedalPressed)
for event in sm['onroadEvents'])):
return False
native = _configured_transport_native(sm, cp, self.session, now_ns, allow_restricted_lateral=True)
native = _configured_transport_native(sm, cp, self.session, now_ns,
require_longitudinal=not pending_stop, allow_restricted_lateral=True)
if native is None and combined_stop:
newer = _configured_transport_native(sm, cp, self.session, now_ns,
require_longitudinal=False, allow_restricted_lateral=True)
if (newer is not None and newer.pandaInventory and
0 < int(sm.logMonoTime['pandaStates']) < newer.sourcePandaStatesMonoTime and
all(slot.controlsAllowed for slot, config in zip(newer.pandaInventory, cp.safetyConfigs, strict=True)
if str(config.safetyModel) not in ('silent', 'noOutput'))):
native = newer
return bool(native is not None and native.pandaInventory and native.requestedLateral and
(not native.lateralAllowed or subaru_stop and not axis.desiredLateral and not axis.lateralActive) and
(not native.requestedLongitudinal and not combined_stop or
(pending_stop or not native.requestedLongitudinal and not combined_stop or
combined_stop and native.requestedLongitudinal and native.longitudinalAllowed) and
native.observedMonoTime >= self.lateral_native_ns and
(native.pandaSerial, tuple(p.hardwareSerial for p in native.pandaInventory)) == self.lateral_inventory)
@@ -200,6 +200,7 @@ def controls_for(cp, mode=None, enabled=False):
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'),
):
params = Params()
params.put_bool('OpenpilotEnabledToggle', True, block=True)
selected = published_cp(cp, params)
params.put('CarParams', selected.to_bytes(), block=True)
params.put(KEY, replace_preference(None, cp, enabled), block=True)
@@ -840,12 +840,10 @@ border-radius: var(--radius-full); color: var(--text-muted);
.gx-auth form .gx-btn { justify-self: start; }
/* Focused System Monitor adaptation from frozen Galaxy material.css:2424-2443. */
.gx-view, .gx-monitor { min-width: 0; max-width: 100%; }
/* Every tool page shares the Colors & Layout rhythm: one grid, one gap. */
.gx-view, .gx-driving, .gx-long-curves, .gx-vehicle { display: grid; gap: var(--sp-4); align-content: start; }
.gx-view > *, .gx-driving > *, .gx-long-curves > *, .gx-vehicle > * { margin-block: 0; }
.gx-view h2, .gx-monitor h2 { margin-top: 0; }
.gx-note { color: var(--text-muted); font-size: var(--fs-sm); }
/* Shared banners: message first, optional actions on their own row. */
.gx-alert { --notice-accent: var(--primary); align-items: center; backdrop-filter: var(--glass-blur); -webkit-backdrop-filter: var(--glass-blur); border: 1px solid var(--glass-border); border-left: 4px solid var(--notice-accent); border-radius: var(--radius-md); display: flex; gap: var(--sp-3); width: 100%; margin: 0 0 var(--sp-4); padding: var(--sp-4); background: color-mix(in srgb, var(--notice-accent) 12%, transparent); }
.gx-alert--warn { --notice-accent: var(--warning); }
.gx-alert--danger { --notice-accent: var(--error); }
@@ -863,7 +861,6 @@ border-radius: var(--radius-full); color: var(--text-muted);
.gx-alert__actions .gx-btn { max-width: 100%; white-space: normal; overflow-wrap: anywhere; padding-block: var(--sp-2); box-shadow: none; }
.gx-alert .gx-spinner { border-top-color: var(--notice-accent); }
.gx-settings > .gx-card, .gx-settings > .gx-alert, .gx-stack > .gx-card, .gx-stack > .gx-alert { margin-bottom: 0; }
/* Shared submenu cube so Tools, Logs and Plots render identically. */
.gx-menu-tile {
align-items: center;
background: var(--surface-container);
@@ -1272,7 +1269,6 @@ input[type="checkbox"]:not(.gx-switch input):checked { background: var(--primary
input[type="checkbox"]:not(.gx-switch input):checked::after { content: ""; display: block; width: .3rem; height: .6rem; border: solid var(--on-primary); border-width: 0 2px 2px 0; transform: translate(.3rem, .08rem) rotate(45deg); }
input[type="checkbox"]:not(.gx-switch input):focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }
input[type="checkbox"]:disabled { opacity: .4; cursor: not-allowed; }
/* Shared action groups wrap while keeping their controls within the page. */
.gx-btn { white-space: normal; max-width: 100%; overflow-wrap: anywhere; }
.gx-actions, .gx-tabs { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; min-width: 0; max-width: 100%; }
.gx-actions > * { max-width: 100%; }
@@ -1547,7 +1543,6 @@ border-top: 1px solid var(--outline-variant);
.gx-update-progress__fill--indeterminate { width: 35%; animation: gx-update-progress 1.6s ease-in-out infinite; }
@keyframes gx-update-progress { from { transform: translateX(-100%); } to { transform: translateX(385%); } }
@media (prefers-reduced-motion: reduce) { .gx-update-progress__fill--indeterminate { width: 100%; animation: none; opacity: .6; } }
/* Composition utilities: shared page chrome and controls need no inline styles. */
.gx-panel { padding: var(--panel-padding); min-width: 0; }
.gx-card.gx-panel > .gx-section__header { margin: calc(-1 * var(--panel-padding)) calc(-1 * var(--panel-padding)) var(--sp-4); }
.gx-panel > :first-child { margin-top: 0; }
@@ -1658,7 +1653,6 @@ progress { max-width: 100%; accent-color: var(--primary); }
.gx-spotify-now__progress { display: grid; gap: 2px; }
.gx-spotify-now__controls { align-items: center; justify-content: center; flex-wrap: wrap; }
.gx-spotify-now__play { min-width: 64px; font-size: 1.4rem; }
/* Auto Learn results use existing cards and plot colors. */
.gx-flm__facts { display: flex; flex-wrap: wrap; gap: 1rem 2rem; }
.gx-flm__facts dd { margin: .3rem 0 0; }
.gx-flm__table-wrap { overflow-x: auto; max-width: 100%; }
@@ -1,4 +1,4 @@
"""Untuned EV6 lateral selection and steering command boundaries."""
"""EV6 vehicle-policy defaults and steering command boundaries."""
import unittest
@@ -15,6 +15,7 @@ from opendbc.car.hyundai.values import CAR, DBC, HyundaiFlags, CarControllerPara
from opendbc.car.lateral import apply_driver_steer_torque_limits
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
from openpilot.starpilot.lateral.torque_extension import selected_policy
from openpilot.starpilot.lateral.hkg_vehicle_policy import HKGVehicleTorquePolicy
from openpilot.starpilot.lateral.torque_runtime import runtime_enabled
@@ -26,7 +27,7 @@ def ev6(lka_steering: bool):
class TestEV6DefaultLateral(unittest.TestCase):
def test_hda_topologies_keep_vehicle_tune_and_generic_controller(self):
def test_hda_topologies_keep_vehicle_tune_and_vehicle_policy(self):
for lka_steering in (False, True):
with self.subTest(lka_steering=lka_steering), OpenpilotPrefix():
cp = ev6(lka_steering)
@@ -40,7 +41,8 @@ class TestEV6DefaultLateral(unittest.TestCase):
self.assertFalse(runtime_enabled(cp, Params()))
self.assertEqual(cp.lateralTuning.to_dict(), original_tune)
lateral = LatControlTorque(cp.as_reader(), CarInterface(cp), DT_CTRL)
self.assertIsNone(selected_policy(lateral))
self.assertIsInstance(selected_policy(lateral), HKGVehicleTorquePolicy)
self.assertEqual(selected_policy(lateral).profile, "kia_ev6")
self.assertAlmostEqual(lateral.torque_params.latAccelFactor, cp.lateralTuning.torque.latAccelFactor)
controller = CarController(DBC[cp.carFingerprint], cp)
self.assertIsNone(controller.ioniq6_longitudinal)
@@ -28,7 +28,7 @@ def _legacy_friction(controller, error, deadzone, threshold, _cs, _setpoint):
def _rows(car, sequence, *, profile='2023', matched_frozen_cp=False, passive=False, direct_original_controller=False, turn_assist=True,
roll=None):
roll=None, controller_mode=None):
cp = interfaces[car].get_non_essential_params(car)
cp.passive = passive
if matched_frozen_cp:
@@ -44,7 +44,7 @@ def _rows(car, sequence, *, profile='2023', matched_frozen_cp=False, passive=Fal
versions[0].fwVersion = b'IONIQ6-230915'
versions[1].fwVersion = b'IONIQ6-240206'
ci = interfaces[car](cp)
controller = LatControlTorque(cp.as_reader(), ci, DT_CTRL, turn_assist=turn_assist)
controller = LatControlTorque(cp.as_reader(), ci, DT_CTRL, turn_assist=turn_assist, controller_mode=controller_mode)
if direct_original_controller:
# The older frozen vectors called the controller directly, before the
# original controlsd drive loop replaced the PID gain curve each tick.
@@ -178,7 +178,8 @@ class Ioniq6PolicyTests(unittest.TestCase):
parent = json.loads((TESTDATA / 'non_ioniq_parent.json').read_text())
for car in (TOYOTA.TOYOTA_COROLLA_TSS2, HYUNDAI.HYUNDAI_IONIQ_5):
with self.subTest(car=str(car)):
controller, actual = _rows(car, sequence, passive=car == TOYOTA.TOYOTA_COROLLA_TSS2, turn_assist=False)
controller, actual = _rows(car, sequence, passive=car == TOYOTA.TOYOTA_COROLLA_TSS2, turn_assist=False,
controller_mode='standard')
self.assertIsNone(selected_policy(controller))
for row, expected in zip(actual, parent[str(car)], strict=True):
for value, reference in zip(row, expected, strict=True):
+1 -1
View File
@@ -56,7 +56,7 @@ def test_weather_is_nonblocking_single_flight_cached_and_expires():
original(point)
finally:
finished.set()
client._refresh = refresh
client._refresh = Mock(side_effect=refresh)
try:
start = time.monotonic()
assert client.observe((41., -87.)) is None
@@ -0,0 +1,20 @@
import time
from contextlib import suppress
from openpilot.common.swaglog import cloudlog
class HardwareLoopTiming:
def __init__(self):
self.phase = ''
self.wall_ns = 0
self.cpu_ns = 0
def mark(self, phase: str) -> None:
wall_ns, cpu_ns = time.monotonic_ns(), time.thread_time_ns()
elapsed_ns = wall_ns - self.wall_ns
if self.phase and elapsed_ns >= 500_000_000:
with suppress(OSError):
cloudlog.event('hardwared.slow_phase', phase=self.phase,
elapsedMs=elapsed_ns / 1e6, threadCpuMs=(cpu_ns - self.cpu_ns) / 1e6, error=True)
self.phase, self.wall_ns, self.cpu_ns = phase, wall_ns, cpu_ns
+18
View File
@@ -30,6 +30,7 @@ from openpilot.common.linux import LinuxSystemStats
from openpilot.system.loggerd.config import get_available_percent
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware.power_monitoring import PowerMonitoring
from openpilot.system.hardware.hardware_timing import HardwareLoopTiming
from openpilot.system.hardware.fan_controller import FanController
from openpilot.system.hardware.chestnut.monitoring import chestnut_state_thread
from openpilot.system.hardware.chestnut.status import ChestnutStatus
@@ -224,6 +225,7 @@ def hw_state_thread(end_event, hw_queue):
def hardware_thread(end_event, hw_queue) -> None:
system_stats = LinuxSystemStats() if sys.platform == "linux" else None
timing = HardwareLoopTiming()
pm = messaging.PubMaster(['deviceState'])
sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates", "chestnutState"], poll="pandaStates")
@@ -283,8 +285,10 @@ def hardware_thread(end_event, hw_queue) -> None:
branch = get_short_branch()
while not end_event.is_set():
timing.mark("panda_poll")
sm.update(PANDA_STATES_TIMEOUT)
timing.mark("drive_mode")
pandaStates = sm['pandaStates']
peripheralState = sm['peripheralState']
@@ -324,6 +328,7 @@ def hardware_thread(end_event, hw_queue) -> None:
if (sm.frame % round(SERVICE_LIST['pandaStates'].frequency * DT_HW) != 0) and not ign_edge:
continue
timing.mark("thermal_read")
msg = messaging.new_message('deviceState', valid=True)
msg.deviceState = thermal_config.get_msg()
msg.deviceState.deviceType = HARDWARE.get_device_type()
@@ -333,6 +338,7 @@ def hardware_thread(end_event, hw_queue) -> None:
except queue.Empty:
pass
timing.mark("system_stats")
memory_usage = system_stats.memory_usage_percent() if system_stats is not None else 0.
cpu_usage = system_stats.cpu_usage_percent() if system_stats is not None else []
@@ -352,8 +358,10 @@ def hardware_thread(end_event, hw_queue) -> None:
msg.deviceState.modemTempC = last_hw_state.modem_temps
timing.mark("brightness")
msg.deviceState.screenBrightnessPercent = HARDWARE.get_screen_brightness()
timing.mark("usb_chestnut")
set_usb_state(msg.deviceState, last_hw_state.usb_state)
chestnut.update(started_ts is None, last_hw_state.usb_state)
chestnut_state = sm["chestnutState"]
@@ -374,6 +382,7 @@ def hardware_thread(end_event, hw_queue) -> None:
all_comp_temp = all_temp_filter.update(max(temp_sources))
msg.deviceState.maxTempC = all_comp_temp
timing.mark("fan_control")
msg.deviceState.fanSpeedPercentDesired = fan_controller.update(all_comp_temp, onroad_conditions["ignition"])
is_offroad_for_5_min = (started_ts is None) and ((not started_seen) or (off_ts is None) or (time.monotonic() - off_ts > 60 * 5))
@@ -391,6 +400,7 @@ def hardware_thread(end_event, hw_queue) -> None:
# **** starting logic ****
timing.mark("startup_conditions")
startup_conditions["up_to_date"] = params.get("Offroad_ConnectivityNeeded") is None or params.get_bool("DisableUpdates") or params.get_bool("SnoozeUpdate")
startup_conditions["no_excessive_actuation"] = params.get("Offroad_ExcessiveActuation") is None
startup_conditions["not_uninstalling"] = not params.get_bool("DoUninstall")
@@ -413,6 +423,7 @@ def hardware_thread(end_event, hw_queue) -> None:
onroad_conditions["device_temp_good"] = thermal_status < ThermalStatus.critical
extra_text = f"{offroad_comp_temp:.1f}C"
show_alert = (not onroad_conditions["device_temp_good"] or not startup_conditions["device_temp_engageable"]) and onroad_conditions["ignition"]
timing.mark("alerts")
set_offroad_alert_if_changed("Offroad_TemperatureTooHigh", show_alert, extra_text=extra_text)
update_jetlink_alert()
@@ -426,6 +437,7 @@ def hardware_thread(end_event, hw_queue) -> None:
startup_conditions["registered_device"] = params.get("DongleId") != UNREGISTERED_DONGLE_ID
# Handle offroad/onroad transition
timing.mark("engagement")
should_start = resolve_start(drive_mode, onroad_conditions, startup_conditions, already_started=started_ts is not None)
if should_start != should_start_prev or (count == 0):
@@ -444,6 +456,7 @@ def hardware_thread(end_event, hw_queue) -> None:
except Exception:
pass
timing.mark("power_save")
should_pwrsave = not (onroad_conditions["ignition"] or should_start) and msg.deviceState.screenBrightnessPercent < 1e-3
if should_pwrsave != pwrsave or (count == 0):
HARDWARE.set_power_save(should_pwrsave)
@@ -479,12 +492,15 @@ def hardware_thread(end_event, hw_queue) -> None:
# Offroad power monitoring
voltage = None if peripheralState.pandaType == log.PandaState.PandaType.unknown else peripheralState.voltage
timing.mark("power_read")
power_monitor.calculate(voltage, onroad_conditions["ignition"])
msg.deviceState.offroadPowerUsageUwh = power_monitor.get_power_used()
msg.deviceState.carBatteryCapacityUwh = max(0, power_monitor.get_car_battery_capacity())
timing.mark("power_draw")
current_power_draw = HARDWARE.get_current_power_draw()
msg.deviceState.powerDrawW = current_power_draw
timing.mark("som_power")
som_power_draw = HARDWARE.get_som_power_draw()
msg.deviceState.somPowerDrawW = som_power_draw
@@ -494,6 +510,7 @@ def hardware_thread(end_event, hw_queue) -> None:
cloudlog.warning(f"shutting device down, offroad since {shutdown_off_ts}")
params.put_bool("DoShutdown", True, block=True)
timing.mark("publish")
msg.deviceState.started = started_ts is not None
msg.deviceState.startedMonoTime = int(1e9*(started_ts or 0))
@@ -524,6 +541,7 @@ def hardware_thread(end_event, hw_queue) -> None:
except Exception:
cloudlog.exception("failed to save offroad status")
timing.mark("bookkeeping")
params.put_bool("NetworkMetered", msg.deviceState.networkMetered)
now_ts = time.monotonic()
@@ -0,0 +1,31 @@
from unittest import TestCase, mock
from openpilot.system.hardware import hardware_timing as timing
class TestHardwareTiming(TestCase):
def test_slow_phase_preserves_original_wall_and_thread_cpu_clocks(self):
for elapsed_ns, reported in ((499_999_999, False), (500_000_000, True), (5_100_019_763, True)):
with self.subTest(elapsed_ns=elapsed_ns), \
mock.patch.object(timing.time, 'monotonic_ns', side_effect=[1_000_000_000, 1_000_000_000 + elapsed_ns]), \
mock.patch.object(timing.time, 'thread_time_ns', side_effect=[80_000_000, 90_000_000]), \
mock.patch.object(timing.cloudlog, 'event') as report:
watch = timing.HardwareLoopTiming()
watch.mark('thermal_read')
report.assert_not_called()
watch.mark('system_stats')
self.assertEqual(bool(report.call_count), reported)
if reported:
report.assert_called_once_with('hardwared.slow_phase', phase='thermal_read', elapsedMs=elapsed_ns / 1e6,
threadCpuMs=10., error=True)
self.assertEqual(watch.phase, 'system_stats')
def test_logging_failure_does_not_interrupt_hardware_work(self):
with mock.patch.object(timing.time, 'monotonic_ns', side_effect=[1, 5_100_000_001]), \
mock.patch.object(timing.time, 'thread_time_ns', side_effect=[1, 10_000_001]), \
mock.patch.object(timing.cloudlog, 'event', side_effect=OSError('logging unavailable')):
watch = timing.HardwareLoopTiming()
watch.mark('power_read')
watch.mark('publish')
self.assertEqual(watch.phase, 'publish')
self.assertEqual(watch.wall_ns, 5_100_000_001)
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
View File
@@ -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.
+1 -1
View File
@@ -1 +1 @@
DEV-5d6e6afc-Pf38f504f-DEBUG
DEV-3093e5b7-Pf38f504f-DEBUG
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "1364c5a835bd115aea10b2847f46703f087444fd3204e07e508188ad5c97115d", "schema_version": 1}
{"protected_inventory_sha256": "f1741daeb7b18776c77b38c839c9a6e97bfb97229851365246afdf6e28936f60", "schema_version": 1}