Restore recorded Elantra diagnostic admission and Panda fault logging

This commit is contained in:
firestar5683
2026-10-26 22:05:26 -05:00
parent bbbed56e54
commit 7d218d0a4b
14 changed files with 188 additions and 5 deletions
+3
View File
@@ -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)
Binary file not shown.
Binary file not shown.
+7 -3
View File
@@ -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<size_t>(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);
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,53 @@
#include <algorithm>
#include <cstring>
#include <memory>
#include <string>
#include <vector>
#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<size_t>(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<DebugHandle> handle) : Panda(std::move(handle), cereal::PandaState::PandaType::CUATRO) {}
};
void test_debug_transport() {
auto handle = std::make_unique<DebugHandle>(); 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); }
+9 -1
View File
@@ -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
@@ -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)
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "c822f9b15f3f13ee17c2a7c0ceb3062caffb81ffebab07965943044cc00c01f2", "schema_version": 1}
{"protected_inventory_sha256": "85f436188fc0877fc0e2da301331a3c74e2d068985c5f1c5f962c9b3bf541deb", "schema_version": 1}