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 |
@@ -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())
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -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.
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": "1364c5a835bd115aea10b2847f46703f087444fd3204e07e508188ad5c97115d", "schema_version": 1}
|
||||
{"protected_inventory_sha256": "f1741daeb7b18776c77b38c839c9a6e97bfb97229851365246afdf6e28936f60", "schema_version": 1}
|
||||
|
||||
Reference in New Issue
Block a user