diff --git a/openpilot/selfdrive/pandad/SConscript b/openpilot/selfdrive/pandad/SConscript index 7065c076b9..16ad9f2e19 100644 --- a/openpilot/selfdrive/pandad/SConscript +++ b/openpilot/selfdrive/pandad/SConscript @@ -34,3 +34,6 @@ if arch != "Darwin": if GetOption('extras') or any(target.startswith('openpilot/selfdrive/pandad/tests/test_preap_pedal_startup') for target in COMMAND_LINE_TARGETS): env.Program('tests/test_preap_pedal_startup', ['tests/test_preap_pedal_startup.cc', env.Object('tests/preap_panda_safety.o', 'panda_safety.cc')], LIBS=[panda] + libs + ['dl']) + + if GetOption('extras') or any(target.startswith('openpilot/selfdrive/pandad/tests/test_panda_debug') for target in COMMAND_LINE_TARGETS): + env.Program('tests/test_panda_debug', ['tests/test_panda_debug.cc'], LIBS=[panda] + libs) diff --git a/openpilot/selfdrive/pandad/libpanda.a b/openpilot/selfdrive/pandad/libpanda.a index f45a467c69..e9d2230767 100644 Binary files a/openpilot/selfdrive/pandad/libpanda.a and b/openpilot/selfdrive/pandad/libpanda.a differ diff --git a/openpilot/selfdrive/pandad/main.o b/openpilot/selfdrive/pandad/main.o new file mode 100644 index 0000000000..7a7d6fd791 Binary files /dev/null and b/openpilot/selfdrive/pandad/main.o differ diff --git a/openpilot/selfdrive/pandad/panda.cc b/openpilot/selfdrive/pandad/panda.cc index 6c7b563fc3..408cf2b19d 100644 --- a/openpilot/selfdrive/pandad/panda.cc +++ b/openpilot/selfdrive/pandad/panda.cc @@ -64,9 +64,13 @@ std::string Panda::serial_read(int port_number) { std::string ret; char buffer[USBPACKET_MAX_SIZE] = {}; - while (true) { - int bytes_read = handle->control_read(0xe0, port_number, 0, (unsigned char *)buffer, USBPACKET_MAX_SIZE); - if (bytes_read <= 0) { + // Firmware exposes one debug ring; bound each drain so CAN work keeps its cadence. + (void)port_number; + constexpr size_t max_debug_bytes = 1024; + while (ret.size() < max_debug_bytes) { + const uint16_t length = std::min(USBPACKET_MAX_SIZE, max_debug_bytes - ret.size()); + int bytes_read = handle->control_read(0xb6, 0, 0, (unsigned char *)buffer, length); + if (bytes_read <= 0 || bytes_read > length) { break; } ret.append(buffer, bytes_read); diff --git a/openpilot/selfdrive/pandad/panda.o b/openpilot/selfdrive/pandad/panda.o new file mode 100644 index 0000000000..18c215943b Binary files /dev/null and b/openpilot/selfdrive/pandad/panda.o differ diff --git a/openpilot/selfdrive/pandad/panda_safety.o b/openpilot/selfdrive/pandad/panda_safety.o new file mode 100644 index 0000000000..6e662e400b Binary files /dev/null and b/openpilot/selfdrive/pandad/panda_safety.o differ diff --git a/openpilot/selfdrive/pandad/panda_usb.o b/openpilot/selfdrive/pandad/panda_usb.o new file mode 100644 index 0000000000..c65e72a8c3 Binary files /dev/null and b/openpilot/selfdrive/pandad/panda_usb.o differ diff --git a/openpilot/selfdrive/pandad/pandad b/openpilot/selfdrive/pandad/pandad index 9483a2fde5..1acd8dd5a5 100755 Binary files a/openpilot/selfdrive/pandad/pandad and b/openpilot/selfdrive/pandad/pandad differ diff --git a/openpilot/selfdrive/pandad/pandad.o b/openpilot/selfdrive/pandad/pandad.o new file mode 100644 index 0000000000..53d48ea10e Binary files /dev/null and b/openpilot/selfdrive/pandad/pandad.o differ diff --git a/openpilot/selfdrive/pandad/spi.o b/openpilot/selfdrive/pandad/spi.o new file mode 100644 index 0000000000..100bd3c127 Binary files /dev/null and b/openpilot/selfdrive/pandad/spi.o differ diff --git a/openpilot/selfdrive/pandad/tests/test_panda_debug.cc b/openpilot/selfdrive/pandad/tests/test_panda_debug.cc new file mode 100644 index 0000000000..c3225e069c --- /dev/null +++ b/openpilot/selfdrive/pandad/tests/test_panda_debug.cc @@ -0,0 +1,53 @@ +#include +#include +#include +#include +#include + +#include "common/tests/native_test.h" +#include "selfdrive/pandad/panda.h" + +class DebugHandle : public PandaCommsHandle { +public: + std::string payload; + int calls = 0; + int writes = 0; + bool endless = false; + int error = 0; + int control_write(uint8_t, uint16_t, uint16_t, unsigned int) override { ++writes; return 0; } + int control_read(uint8_t request, uint16_t p1, uint16_t p2, unsigned char *out, uint16_t length, unsigned int) override { + ++calls; + CHECK(request == 0xb6); + CHECK(p1 == 0 && p2 == 0 && length > 0 && length <= 64); + if (error) return error; + if (endless) { memset(out, 'x', length); return length; } + const size_t count = std::min(length, payload.size()); + memcpy(out, payload.data(), count); + payload.erase(0, count); + return count; + } + int bulk_write(unsigned char, unsigned char *, int, unsigned int) override { ++writes; return 0; } + int bulk_read(unsigned char, unsigned char *, int, unsigned int) override { return 0; } + void cleanup() override {} +}; +class DebugPanda : public Panda { +public: + explicit DebugPanda(std::unique_ptr handle) : Panda(std::move(handle), cereal::PandaState::PandaType::CUATRO) {} +}; +void test_debug_transport() { + auto handle = std::make_unique(); auto *io = handle.get(); + DebugPanda panda(std::move(handle)); + io->payload = std::string(83, 'a') + "Temporary fault occurred: 0x00000004\r\n"; + const std::string expected = io->payload; + CHECK(panda.serial_read() == expected); + CHECK(io->calls == 3 && io->payload.empty() && io->writes == 0); + io->calls = 0; CHECK(panda.serial_read().empty()); CHECK(io->calls == 1); + io->calls = 0; io->endless = true; + CHECK(panda.serial_read() == std::string(1024, 'x')); + CHECK(io->calls == 16 && io->writes == 0); + io->endless = false; io->error = -1; io->calls = 0; + CHECK(panda.serial_read().empty()); CHECK(io->calls == 1); + io->error = 65; io->calls = 0; + CHECK(panda.serial_read().empty()); CHECK(io->calls == 1 && io->writes == 0); +} +int main() { return run_native_test(test_debug_transport); } diff --git a/openpilot/starpilot/aol/panda_faults.py b/openpilot/starpilot/aol/panda_faults.py index cd1f4ad4f4..ce0de568c7 100644 --- a/openpilot/starpilot/aol/panda_faults.py +++ b/openpilot/starpilot/aol/panda_faults.py @@ -21,9 +21,17 @@ def _diagnostic_mask(cp): int(cp.flags) & int(HondaFlags.BOSCH) and qualified(cp, marked_only=True)) else 0 if cp.brand == 'hyundai': from opendbc.car.hyundai.values import CAR, HyundaiFlags + from opendbc.car.hyundai.classic_scc_aol import qualified + if cp.carFingerprint == CAR.HYUNDAI_ELANTRA_2021: + # Route08's stock-SCC firmware-matched profile: Dom reports this latched + # camera-side CAN1 diagnostic without withdrawing actuator authority. + # Keep the raw bit and require the ordinary native/RX/heartbeat guards. + recorded_flags = HyundaiFlags.SEND_LFA | HyundaiFlags.USE_FCA | HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.HAS_LDA_BUTTON + return (1 << 2) if (str(cp.fingerprintSource) == 'fw' and int(cp.flags) == int(recorded_flags) and + len(cp.safetyConfigs) == 1 and int(cp.safetyConfigs[0].safetyParam) == 0xC00 and + cp.alternativeExperience == 32 and qualified(cp, marked_only=True)) else 0 if cp.carFingerprint != CAR.HYUNDAI_SONATA_HYBRID: return 0 - from opendbc.car.hyundai.classic_scc_aol import qualified recorded_flags = (HyundaiFlags.HYBRID | HyundaiFlags.SEND_LFA | HyundaiFlags.USE_FCA | HyundaiFlags.MANDO_RADAR | HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.HAS_LDA_BUTTON) return (1 << 3) if (int(cp.flags) == int(recorded_flags) and diff --git a/openpilot/starpilot/aol/tests/test_elantra_irq_admission.py b/openpilot/starpilot/aol/tests/test_elantra_irq_admission.py new file mode 100644 index 0000000000..3f22a0cf63 --- /dev/null +++ b/openpilot/starpilot/aol/tests/test_elantra_irq_admission.py @@ -0,0 +1,115 @@ +"""Elantra route08: retain Dom diagnostic semantics without granting native authority.""" +from contextlib import ExitStack +from dataclasses import replace +from unittest import mock + +import pytest +from opendbc.car import gen_empty_fingerprint +from opendbc.car.hyundai.classic_scc_aol import aol_word, qualified +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR +from openpilot.cereal import log, messaging +from openpilot.common.prefix import OpenpilotPrefix +from openpilot.starpilot.aol.panda_faults import blocking_fault_list, blocking_fault_mask +from openpilot.starpilot.aol.runtime import current_native +from openpilot.starpilot.aol.tests import test_ioniq_availability as fixture +from openpilot.starpilot.aol.tests.test_ordinary_source_availability import settings + + +def recorded_cp(): + fingerprint = gen_empty_fingerprint() + fingerprint[0].update({0x391: 8, 0x38d: 8}) + fingerprint[2][0x485] = 4 + cp = CarInterface.get_params(CAR.HYUNDAI_ELANTRA_2021, fingerprint, [], False, False, False) + cp.fingerprintSource = 'fw' + cp.safetyConfigs[0].safetyParam = aol_word(cp) + cp.alternativeExperience = 32 + assert int(cp.flags) == 16843136 and cp.safetyConfigs[0].safetyParam == 0xC00 + assert qualified(cp, marked_only=True) + return cp + + +@pytest.mark.parametrize('full', [False, True]) +@pytest.mark.parametrize('fault', ['none', 'rx', 'relay', 'denied', 'request', 'expired-wire']) +def test_recorded_irq_through_selfdrived_and_controls_keeps_native_vetoes(full, fault): + cp = recorded_cp() + before = cp.to_dict() + case = fixture.IoniqAvailabilityTests(methodName='runTest') + with ExitStack() as cleanup: + cleanup.callback(case.doCleanups) + cleanup.enter_context(OpenpilotPrefix()) + sd, controls, drive = case._authority_callers(full, cp=cp, genuine_events=True) + new_message = messaging.new_message + def with_irq(service=None, size=None, **kwargs): + message = new_message(service, size, **kwargs) + if service == 'pandaStates': + for panda in message.pandaStates: + panda.faults = ['interruptRateCan1'] + return message + encode = fixture.encode_safety + def wire_irq(state): + state = replace(state, pandaInventory=tuple(replace(slot, faults=slot.faults | 4) for slot in state.pandaInventory)) + if fault == 'expired-wire': + state = replace(state, observedMonoTime=state.observedMonoTime - 300_000_000, + validUntilMonoTime=state.validUntilMonoTime - 300_000_000) + return encode(state) + cleanup.enter_context(mock.patch.object(messaging, 'new_message', with_irq)) + cleanup.enter_context(mock.patch.object(fixture, 'encode_safety', wire_irq)) + for tick in range(3): + stamp = 10_000_000_000 + tick * 10_000_000 + options = settings('combined' if full else 'lateral_only', stamp, tick + 1) + observed = fixture.car_state() + observed.cruiseState.enabled = full + options['observed_state'] = observed + options.update(panda_fault=fault == 'rx', relay_fault=fault == 'relay', long_allowed=full) + if fault == 'denied': + options.update(allowed=False, long_allowed=False) + if fault == 'request': + options.update(native_request_lat=False, native_request_long=False) + axis, state, command = drive(tick, **options) + if fault == 'none': + assert axis.lateralActive and command.latActive, 'RECORDED_ELANTRA_IRQ_DIAGNOSTIC_REJECTED' + assert log.OnroadEvent.EventName.controlsMismatch not in sd.events.names + native = current_native(sd.sm, sd.CP, now_ns=stamp, axis_session_id=sd.aol_session_id) + assert native is not None and native.pandaInventory[0].faults == 4 + assert list(sd.sm['pandaStates'][0].faults) == [log.PandaState.FaultType.interruptRateCan1] + assert not axis.longitudinalActive and not command.longActive + assert state.enabled == full + else: + assert not axis.lateralActive and not command.latActive + assert not axis.longitudinalActive and not command.longActive + assert cp.to_dict() == before + + +@pytest.mark.parametrize('name,value', tuple(log.PandaState.FaultType.schema.enumerants.items())) +def test_other_and_mixed_faults_still_block(name, value): + cp = recorded_cp() + expected = name != 'interruptRateCan1' + assert blocking_fault_mask(1 << value, cp) == expected + assert blocking_fault_mask((1 << value) | 4, cp) == expected + assert blocking_fault_list([value], cp) == expected + assert blocking_fault_list([log.PandaState.FaultType.interruptRateCan1, value], cp) == expected + + +@pytest.mark.parametrize('mutation', ['alpha', 'fixed', 'can', 'word', 'unmarked', 'flags', 'identity', 'extra', 'passive']) +def test_unrecorded_configurations_cannot_borrow_diagnostic(mutation): + cp = recorded_cp() + if mutation == 'alpha': + cp.openpilotLongitudinalControl = True + cp.pcmCruise = False + elif mutation in ('fixed', 'can'): + cp.fingerprintSource = mutation + elif mutation == 'word': + cp.safetyConfigs[0].safetyParam |= 4 + elif mutation == 'unmarked': + cp.alternativeExperience = 0 + elif mutation == 'flags': + cp.flags |= 1 << 31 + elif mutation == 'identity': + cp.carFingerprint = CAR.HYUNDAI_SONATA + elif mutation == 'extra': + cp.safetyConfigs = [cp.safetyConfigs[0], cp.safetyConfigs[0]] + else: + cp.passive = True + assert blocking_fault_mask(4, cp) + assert blocking_fault_list([log.PandaState.FaultType.interruptRateCan1], cp) diff --git a/prebuilt.json b/prebuilt.json index eefe74b55f..84237c9b42 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "c822f9b15f3f13ee17c2a7c0ceb3062caffb81ffebab07965943044cc00c01f2", "schema_version": 1} +{"protected_inventory_sha256": "85f436188fc0877fc0e2da301331a3c74e2d068985c5f1c5f962c9b3bf541deb", "schema_version": 1}