From 803de7b64502fab83efc2ceb1a092028efa2ffac Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Sat, 19 Sep 2026 22:05:14 -0700 Subject: [PATCH] Recognize verified passive ELM327 diagnostic startup without waiving faults --- .../prototype/PASSIVE_OBSERVER_SAFETY.md | 36 +++++++++++++++++++ .../prototype/passive_observer_policy.py | 16 +++++++-- .../prototype/test_passive_observer_policy.py | 35 ++++++++++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 roadscore/prototype/PASSIVE_OBSERVER_SAFETY.md diff --git a/roadscore/prototype/PASSIVE_OBSERVER_SAFETY.md b/roadscore/prototype/PASSIVE_OBSERVER_SAFETY.md new file mode 100644 index 0000000000..557b09c6bd --- /dev/null +++ b/roadscore/prototype/PASSIVE_OBSERVER_SAFETY.md @@ -0,0 +1,36 @@ +# Passive observer safety evidence + +The helper accepts two exact observed variants with genuine `CarParams.passive`, +configured noOutput/parameter0, inactive fresh control/state signals and fault-free +pandas: all actual noOutput, or all actual ELM327/parameter1 under the documented +passive startup conditions. It is not authorization, calibration acceptance, +coexistence acceptance, engagement support, or proof of zero CAN traffic. + +The earlier assumption that normal passive startup must reach actual noOutput was +incorrect. Source in `selfdrive/pandad/panda_safety.cc` initializes every panda to +ELM327 parameter1 before waiting for FirmwareQueryDone and ControlsReady. Upstream +`selfdrive/car/card.py` sets ControlsReady inside controls_update, while passive +step skips controls_update. Thus the normal passive path can remain in ELM327. +Parameter1 means no OBD multiplexing; enabling OBD multiplexing changes the primary +panda parameter to0. Only the explicit parameter1 baseline is accepted here. + +The collector supplies current read-only `passive_startup` values: +`controls_ready=false`, `firmware_query_done=true`, +`obd_multiplexing_enabled=false`. Missing helper evidence fails closed; native +Params absent-bool semantics are the collector's responsibility. No helper writes +these flags or attempts to force a safety transition. + +**ELM327 is not noOutput.** `opendbc/safety/modes/elm327.h` permits eight-byte +messages to diagnostic addresses 0x600–0x7FF, 0x18DB33F1, 0x18DAxxF1, and0x24B. +For0x24B it additionally restricts the leading ISO-TP nibble to0–3. Ordinary +allowed diagnostic addresses accept arbitrary payload content; this must not be +described as an all-TX-disabled safety mode. The mode uses nooutput_init and +requires observed controlsAllowed=false, but has its own diagnostic TX hook. +RoadScore's observer integration must continue to publish no CAN/control messages. + +Faults remain an independent blocker, including interruptRateCan2. ELM allowance +does not waive safetyRxChecksInvalid, stale/invalid messages, active/always-on +lateral flags, calibration, model health, local-model placement, resource/link +bounds or parked coexistence. Mixed observed safety modes are rejected as a +transition/unestablished baseline. Mode identity includes actual ELM parameter1, +so a noOutput authorization pin cannot silently carry over to ELM. diff --git a/roadscore/prototype/passive_observer_policy.py b/roadscore/prototype/passive_observer_policy.py index 47f118e62b..bdf6ee9148 100644 --- a/roadscore/prototype/passive_observer_policy.py +++ b/roadscore/prototype/passive_observer_policy.py @@ -41,9 +41,19 @@ def evaluate_passive_observer(snapshot, requested_mode): pandas=snapshot.get('pandas') if not isinstance(pandas,list) or not pandas: reasons.append('Actual panda safety evidence is missing');pandas=[] + actual_modes={p.get('safetyModel') for p in pandas if isinstance(p,dict)} + elm=actual_modes=={'elm327'} + if len(actual_modes)>1:reasons.append('Mixed panda startup safety modes are not an established passive baseline') + if elm: + startup=_mapping(snapshot.get('passive_startup')) + if startup.get('controls_ready') is not False:reasons.append('ELM passive baseline requires actual ControlsReady=false') + if startup.get('firmware_query_done') is not True:reasons.append('ELM passive baseline requires completed firmware query') + if startup.get('obd_multiplexing_enabled') is not False:reasons.append('ELM parameter-1 baseline requires OBD multiplexing disabled') for index,panda in enumerate(pandas): if not isinstance(panda,dict):reasons.append(f'Panda {index} evidence is malformed');continue - if panda.get('safetyModel')!='noOutput':reasons.append(f'Panda {index} is not actually noOutput') + if panda.get('safetyModel') not in ('noOutput','elm327'):reasons.append(f'Panda {index} is not in a supported passive safety mode') + if panda.get('safetyModel')=='elm327' and (type(panda.get('safetyParam')) is not int or panda['safetyParam']!=1): + reasons.append(f'Panda {index} is not the expected ELM327 parameter-1 startup state') if panda.get('controlsAllowed') is not False:reasons.append(f'Panda {index} permits or has unknown actuation') if panda.get('faults')!=[]:reasons.append(f'Panda {index} fault state is not explicitly clear') if panda.get('safetyRxChecksInvalid') is not False:reasons.append(f'Panda {index} safety receive checks are invalid or unknown') @@ -60,10 +70,10 @@ def evaluate_passive_observer(snapshot, requested_mode): # baseline hashes. Runtime freshness/booleans are validated above, not hashed. material={'mode':MODE,'passive':cp.get('passive'),'notCar':cp.get('notCar'), 'dashcamOnly':cp.get('dashcamOnly'),'configured_safety':configs, - 'actual_panda_safety':[p.get('safetyModel') if isinstance(p,dict) else None for p in pandas]} + 'actual_panda_safety':[{'model':p.get('safetyModel'),'parameter':p.get('safetyParam') if p.get('safetyModel')=='elm327' else 0} if isinstance(p,dict) else None for p in pandas]} mode_identity=hashlib.sha256(json.dumps(material,sort_keys=True,separators=(',',':')).encode()).hexdigest() if not reasons else None return {'mode':MODE,'safety_eligible':not reasons,'reasons':reasons,'mode_identity':mode_identity, - 'engagement_available':False,'label':'Passive observer — vehicle engagement unavailable'} + 'engagement_available':False,'diagnostic_tx_possible':elm,'actual_passive_variant':'elm327-diagnostic' if elm else 'noOutput','label':'Passive observer — vehicle engagement unavailable'} def authorization_mode_matches(record, assessment): diff --git a/roadscore/prototype/test_passive_observer_policy.py b/roadscore/prototype/test_passive_observer_policy.py index 956a769f7f..abe37c94d8 100644 --- a/roadscore/prototype/test_passive_observer_policy.py +++ b/roadscore/prototype/test_passive_observer_policy.py @@ -69,3 +69,38 @@ def test_explicit_mode_no_auto_inference_and_input_unchanged(): def test_malformed_fields_fail_closed(field): snapshot=healthy();snapshot[field]=None assert not evaluate_passive_observer(snapshot,MODE)['safety_eligible'] + + +def elm_baseline(): + snapshot=healthy();snapshot['pandas'][0].update(safetyModel='elm327',safetyParam=1) + snapshot['passive_startup']={'controls_ready':False,'firmware_query_done':True,'obd_multiplexing_enabled':False} + return snapshot + + +def test_exact_upstream_passive_elm_state_is_diagnostic_not_tx_disabled(): + result=evaluate_passive_observer(elm_baseline(),MODE) + assert result['safety_eligible'] and result['diagnostic_tx_possible'] + assert result['actual_passive_variant']=='elm327-diagnostic' + assert not result['engagement_available'] + nooutput=evaluate_passive_observer(healthy(),MODE) + assert result['mode_identity']!=nooutput['mode_identity'] + +@pytest.mark.parametrize('field,value',[('controls_ready',True),('controls_ready',None),('firmware_query_done',False),('firmware_query_done',None),('obd_multiplexing_enabled',True),('obd_multiplexing_enabled',None)]) +def test_elm_requires_exact_observed_startup_conditions(field,value): + snapshot=elm_baseline();snapshot['passive_startup'][field]=value + assert not evaluate_passive_observer(snapshot,MODE)['safety_eligible'] + +@pytest.mark.parametrize('parameter',(0,2,None,True,'1')) +def test_elm_parameter_mismatch_rejected(parameter): + snapshot=elm_baseline();snapshot['pandas'][0]['safetyParam']=parameter + assert not evaluate_passive_observer(snapshot,MODE)['safety_eligible'] + + +def test_elm_does_not_waive_faults_or_active_flags(): + for field,value in [('faults',['interruptRateCan2']),('controlsAllowed',True),('safetyRxChecksInvalid',True)]: + snapshot=elm_baseline();snapshot['pandas'][0][field]=value + assert not evaluate_passive_observer(snapshot,MODE)['safety_eligible'] + snapshot=elm_baseline();snapshot['control']['latActive']=True + assert not evaluate_passive_observer(snapshot,MODE)['safety_eligible'] + snapshot=elm_baseline();snapshot['pandas'].append(deepcopy(healthy()['pandas'][0])) + assert not evaluate_passive_observer(snapshot,MODE)['safety_eligible']