Preserve FLM learning windows across unchanged steering repeats

The d5 Ioniq 6 logs repeatedly reset source tracking when Card repeats a command that already arrived on time. Discard only a recent unchanged repeat after the existing native CAN verifier succeeds; add no sample, application time or permission.

Use independently admitted pending CarState brackets to complete original poses without waiting for that command to reach Card. Classify stale control source clocks as transient stale evidence, preserving the original bounded study deadline. Replace Python character loops with equivalent strict text predicates.

Validation: focused regression suite and paced d5 replay with original Card command references; actual controller/CAN trial application and worsening-trial rollback. No timing/acceptance threshold changes or claimed road improvement.
This commit is contained in:
firestar5683
2026-10-26 21:55:26 -05:00
parent ed2abcc4b9
commit 6103e0395b
11 changed files with 434 additions and 19 deletions
@@ -327,7 +327,7 @@ class FullrateCollector:
or type(value.control_ns) is not int
or type(value.state_ns) is not int
or not 0 < value.state_ns <= value.control_ns <= now_ns
or value.control_ns - value.state_ns > JOIN_NS
or value.domain != 'normalized_torque' and value.control_ns - value.state_ns > JOIN_NS
or not 1 <= len(value.features) <= 8
or not all(finite(v) for v in (value.requested, value.baseline_prediction, *value.features, *value.applied_coordinates))
or len(value.stage_values) > 40
@@ -410,9 +410,9 @@ class FullrateCollector:
):
self.split('ineligible_control')
return False
if now_ns - value.control_ns > MAX_AGE_NS:
# Expiry alone rejects this row, but may retain an already suspended
# numerical study. Explicit original native refusal is not a timing gap.
if now_ns - value.control_ns > MAX_AGE_NS or value.control_ns - value.state_ns > JOIN_NS:
# Receiver or normalized source-state delay still rejects this row.
# Only inert study data may survive; native refusal is not a timing gap.
if value.native_required and (not value.native_allowed or not identity(value.native_identity) or
not 0 < value.native_observed_ns <= value.control_ns <= value.native_expires_ns <= value.native_observed_ns + 200_000_000):
self.split('original_native_admission_unavailable')
@@ -518,6 +518,11 @@ class FullrateCollector:
current_sources = (consumed_sources_current(command, output) if command.domain == 'normalized_torque' else
command.state_ns <= output.state_ns <= output.apply_ns and output.state_ns - command.state_ns <= JOIN_NS)
if not current_sources:
from openpilot.starpilot.flm.unchanged_output import redundant_late_output
if redundant_late_output(self, command, output):
# The original on-time pair remains untouched. This repeat creates no
# ready row, sample, application time, physical witness or permission.
return
# This pair is unusable, but it does not change the identity/provenance
# of independently sealed historical samples. Withdraw current admission
# and discard the partial window; real context changes still hard-reset.
@@ -13,11 +13,11 @@ MAX_TRIAL_NS = 120_000_000_000
def _text(value, maximum=96):
return type(value) is str and 0 < len(value) <= maximum and value.isascii() and all(32 <= ord(c) < 127 for c in value)
return type(value) is str and 0 < len(value) <= maximum and value.isascii() and value.isprintable()
def _digest(value):
return type(value) is str and len(value) == 64 and all(c in '0123456789abcdef' for c in value)
return type(value) is str and len(value) == 64 and not value.strip('0123456789abcdef')
@dataclass(frozen=True)
@@ -0,0 +1,38 @@
"""Original consumed vehicle states; no actuation or physical permission."""
from openpilot.starpilot.flm.fullrate_collector import ControlEvidence, JOIN_NS, MAX_AGE_NS
def state_brackets(collector, identity, now_ns):
states = {}
def add(control):
if (control.identity, control.generation, control.source_epoch, control.baseline_revision,
control.controller_revision, control.native_identity) != identity:
return
values = dict(control.replay_inputs)
if 'vEgo' in values and 'steeringAngleDeg' in values:
row = (float(values['vEgo']), float(values['steeringAngleDeg']))
if control.state_ns in states and states[control.state_ns] != row:
raise ValueError('conflicting original vehicle state brackets')
states[control.state_ns] = row
for control, _ in collector.ready:
add(control)
# These controls already passed collector admission. Their independently
# recorded CarState can bracket a pose before Card emits their command.
# The observer still requires its original joined native pair; these rows
# supply only speed/angle, and never enter ready or renew any witness.
for control in collector.controls.values():
if (type(control) is not ControlEvidence or control.domain != collector.ready[-1][0].domain
or control.native_required is not collector.ready[-1][0].native_required
or not control.enabled or not control.active or not control.valid
or control.manual or control.override or control.fault or control.saturated
or not 0 < control.state_ns <= control.control_ns <= now_ns
or control.control_ns - control.state_ns > JOIN_NS or now_ns - control.control_ns > MAX_AGE_NS):
continue
if control.native_required and (not control.native_allowed or
not 0 < control.native_observed_ns <= control.control_ns <= control.native_expires_ns <= control.native_observed_ns + 200_000_000):
continue
add(control)
return states
@@ -877,13 +877,8 @@ def original_source_pose_observer(sm, cp, collector, now_ns, *, distinguish_pend
if queue:
# No original pose is waiting after a completed publication. Rebuild
# state brackets only for retained/new rows, after all current guards.
for command, _ in ready:
if (command.identity, command.generation, command.source_epoch, command.baseline_revision,
command.controller_revision, command.native_identity) != current_identity:
continue # State brackets cannot bridge a command/native epoch.
values = dict(command.replay_inputs)
if 'vEgo' in values and 'steeringAngleDeg' in values:
history[command.state_ns] = (float(values['vEgo']), float(values['steeringAngleDeg']))
from openpilot.starpilot.flm.original_pose_brackets import state_brackets
history = state_brackets(collector, current_identity, now_ns)
for index, evidence in enumerate(queue):
before = [stamp for stamp in history if stamp <= (evidence.source_lower_ns or evidence.mono_ns)]
after = [stamp for stamp in history if stamp >= (evidence.source_upper_ns or evidence.mono_ns)]
+5 -5
View File
@@ -132,13 +132,13 @@ def _source_config_known(source, now_ns):
type(source.observed_ns) is int and type(source.expires_ns) is int and
0 < source.observed_ns <= now_ns <= source.expires_ns <= source.observed_ns + SOURCE_LEASE_NS and
type(source.cp_sha256) is str and len(source.cp_sha256) == 64 and
all(char in '0123456789abcdef' for char in source.cp_sha256) and
not source.cp_sha256.strip('0123456789abcdef') and
type(source.reset) is str and 1 <= len(source.reset) <= 64 and
all(char in '0123456789abcdef' for char in source.reset) and
not source.reset.strip('0123456789abcdef') and
type(source.calibration_generation) is str and (not source.calibration_generation or
len(source.calibration_generation) == 32 and all(char in '0123456789abcdef' for char in source.calibration_generation)) and
len(source.calibration_generation) == 32 and not source.calibration_generation.strip('0123456789abcdef')) and
type(source.persistent_source_epoch) is str and (not source.persistent_source_epoch or
len(source.persistent_source_epoch) == 64 and all(char in '0123456789abcdef' for char in source.persistent_source_epoch)) and
len(source.persistent_source_epoch) == 64 and not source.persistent_source_epoch.strip('0123456789abcdef')) and
type(source.manual_baseline) is tuple and len(source.manual_baseline) in (0, 3) and
all(type(value) in (int, float) and math.isfinite(value) for value in source.manual_baseline))
@@ -433,7 +433,7 @@ def run(
return False, now
valid = bool(known and source.enabled and not source.manual and
type(source.source_epoch) is str and len(source.source_epoch) == 64 and
all(char in '0123456789abcdef' for char in source.source_epoch) and
not source.source_epoch.strip('0123456789abcdef') and
(source_cp_sha256 is None or source.cp_sha256 == source_cp_sha256))
if not valid:
source_rejected = True
@@ -0,0 +1,94 @@
"""Timing rejection and inert study recovery; numerical fixtures, no road proof."""
from dataclasses import replace
from types import SimpleNamespace
import pytest
from openpilot.starpilot.flm.fullrate_collector import FullrateCollector
from openpilot.starpilot.flm.tests.test_fullrate_pipeline import SyntheticAdapter, snapshots
from openpilot.starpilot.flm.tests.test_source_study_gaps import active_study
from openpilot.starpilot.flm.tests.test_tracking_source_context import refresh, source_ack
def subject(age=43_305_053, *, pause=True):
owner, now = active_study()
if pause:
owner.interrupt('expired_response_witness')
control, _, _ = snapshots(now)
control = replace(control, domain='normalized_torque', state_ns=now-age, native_required=True,
native_allowed=True, native_identity='4'*64, native_observed_ns=now-50_000_000,
native_expires_ns=now+150_000_000)
collector = FullrateCollector(SyntheticAdapter())
collector.adapter.command_domain = 'normalized_torque'
collector.context = tuple(getattr(control, name) for name in (
'identity', 'generation', 'domain', 'stage_revision', 'response_revision', 'source_epoch',
'controller_mode', 'policy', 'baseline_revision', 'controller_revision', 'turn_assist'))
collector.live_tracking = SimpleNamespace(interrupt=owner.interrupt)
return collector, owner, control, now+37_720_875
@pytest.mark.parametrize('age', (30_000_001, 43_305_053))
@pytest.mark.parametrize('pause', (False, True))
def test_normalized_source_clock_gap_rejects_row_without_erasing_numerical_study(age, pause):
collector, owner, control, now = subject(age, pause=pause)
original = (owner.paused_intent[0] if pause else owner.offer_state).copy()
before, future = owner.paused_intent[1:] if pause else (owner.before, tuple(owner.future))
assert not collector.control(control, now)
assert collector.reason == 'stale_control'
assert not collector.controls and not collector.ready and not collector.recent_controls
assert collector.last_qualified_response == collector.last_apply == 0
assert owner.offer_state['phase'] == 'rollback' and owner.settled_ns is None
assert owner.paused_intent is not None
assert owner.paused_intent[1:] == (before, future)
assert not owner.before and not owner.future and not any(owner.accepted.values())
refresh(owner, now+200_000_000)
rollback = owner.offer(now+200_000_000)
assert rollback is not None and rollback.phase == 'rollback'
assert source_ack(owner, rollback, now+210_000_000)
resumed = owner.offer_state
assert resumed['phase'] == 'trial' and resumed['trial_id'] != original['trial_id']
assert resumed['target_sequence'] > original['target_sequence'] and resumed['published'] is False
for name in ('started_ns', 'deadline_ns', 'alpha', 'required_episodes', 'look_quotas', 'target', 'baseline'):
assert resumed[name] == original[name]
assert (owner.before, tuple(owner.future)) == (before, future) and owner.settled_ns is None
@pytest.mark.parametrize('change,reason', (
({'identity': 'bad'}, 'invalid_control'),
({'requested': float('nan')}, 'invalid_control'),
({'valid': False}, 'ineligible_control'),
({'fault': True}, 'ineligible_control'),
({'override': True}, 'ineligible_control'),
({'manual': True}, 'ineligible_control'),
({'active': False}, 'ineligible_control'),
({'saturated': True}, 'ineligible_control'),
({'native_allowed': False}, 'original_native_admission_unavailable'),
({'native_expires_ns': 1}, 'original_native_admission_unavailable'),
({'source_receipt_ns': 1}, 'invalid_control'),
({'domain': 'torque'}, 'invalid_control'),
))
def test_clock_gap_never_masks_fault_native_structural_or_other_domain_failure(change, reason):
collector, owner, control, now = subject()
assert owner.paused_intent is not None
assert not collector.control(replace(control, **change), now)
assert collector.reason == reason and owner.paused_intent is None
assert not collector.ready and not collector.controls and not any(owner.accepted.values())
@pytest.mark.parametrize('change', ({'identity': '5'*64}, {'source_epoch': '6'*64}, {'baseline_revision': '7'*64}))
def test_clock_gap_with_changed_context_cannot_preserve_old_study(change):
collector, owner, control, now = subject()
assert not collector.control(replace(control, **change), now)
assert collector.training_interruptions['actual_controller_identity_changed'] == 1
assert owner.paused_intent is None and not collector.ready
def test_original_deadline_still_expires_after_source_clock_gap():
collector, owner, control, now = subject()
deadline = owner.paused_intent[0]['deadline_ns']
assert not collector.control(control, now)
refresh(owner, deadline+1)
rollback = owner.offer(deadline+1)
assert rollback is not None and source_ack(owner, rollback, deadline+10_000_001)
assert owner.paused_intent is None and owner.offer_state['phase'] == 'rollback'
assert owner.offer_state['accepted'] and not any(owner.accepted.values())
@@ -0,0 +1,41 @@
"""Equivalent bounded text predicates, including malformed wire text."""
from dataclasses import replace
from types import SimpleNamespace
from openpilot.starpilot.flm.live_trial_contract import _digest, _text
from openpilot.starpilot.flm.runtime_adapter_registry import SourceState
from openpilot.starpilot.flm.steerlearnerd import _source_config_known
class TextSubclass(str):
pass
def old_text(value, maximum):
return type(value) is str and 0 < len(value) <= maximum and value.isascii() and all(32 <= ord(c) < 127 for c in value)
def old_digest(value):
return type(value) is str and len(value) == 64 and all(c in '0123456789abcdef' for c in value)
def test_text_and_digest_match_exact_prior_predicates():
values = [None, False, 0, b'a', [], {}, SimpleNamespace(), TextSubclass('a'), '', 'a' * 193]
alphabet = [chr(i) for i in range(256)] + ['\u200b', '\u2028', '\ud800', '\uffff', '\U0001f642']
for char in alphabet:
values.extend((char, char * 64, 'a' * 31 + char + '0' * 32, 'a' * 63 + char, char + 'a' * 63))
for value in values:
assert _digest(value) is old_digest(value)
for bound in (0, 1, 32, 64, 96, 192):
assert _text(value, bound) is old_text(value, bound)
def test_source_binding_hex_remains_strict():
source = SourceState(True, False, '0', 'a' * 64, 100, 500_000_100, 'b' * 64,
calibration_generation='c' * 32, persistent_source_epoch='d' * 64)
assert _source_config_known(source, 200)
for field in ('cp_sha256', 'reset', 'calibration_generation', 'persistent_source_epoch'):
original = getattr(source, field)
for bad in ('g', 'A', ' ', '\n', '\x00', '\u00e9'):
assert not _source_config_known(replace(source, **{field: original[:-1] + bad}), 200)
assert not _source_config_known(source, 500_000_101)
@@ -0,0 +1,63 @@
"""A consumed state is usable before its command's independent CAN receipt."""
from dataclasses import replace
import pytest
from openpilot.starpilot.flm.original_source_capture import OriginalSourcePose, validate_pose
from openpilot.starpilot.flm.physical_observation import PendingPhysical
from openpilot.starpilot.flm.tests.test_original_pose_work import BASE, fixture, observe, packet, pair
def pending_right(collector, sm, **changes):
stamp = BASE + 10_000_000
sm.update_msgs(stamp / 1e9, [packet('carState', stamp).as_reader()])
control, _ = pair(stamp, speed=22., angle=2.)
control = replace(control, **changes)
collector.controls[control.control_ns] = control
return control
def test_pose_uses_original_pending_state_without_admitting_its_actuation():
with fixture() as (collector, cp, sm):
assert type(observe(collector, cp, sm, BASE)) is PendingPhysical
original = tuple(collector.ready)
control = pending_right(collector, sm)
row = observe(collector, cp, sm, BASE + 12_000_000)
assert type(row) is OriginalSourcePose
validate_pose(row, BASE + 12_000_000)
assert (row.speed_mps, row.steering_angle_deg) == (21., 1.)
assert row.right_state_ns == control.state_ns
assert (row.native_control_ns, row.native_apply_ns) == (original[-1][0].control_ns, original[-1][1].apply_ns)
assert tuple(collector.ready) == original
assert collector.controls[control.control_ns] is control
assert (row.mono_ns, row.received_ns, row.observed_ns) == (BASE, BASE, BASE)
@pytest.mark.parametrize('changes', [
{'identity': '9' * 64}, {'generation': 2}, {'source_epoch': '9' * 64},
{'baseline_revision': 'other'}, {'controller_revision': 'other'}, {'native_identity': '9' * 64},
{'enabled': False}, {'active': False}, {'valid': False}, {'manual': True},
{'override': True}, {'fault': True}, {'saturated': True}, {'native_allowed': False},
{'control_ns': BASE + 12_000_001}, {'control_ns': BASE + 41_000_000},
{'native_expires_ns': BASE + 10_000_000}, {'native_required': False}, {'domain': 'steering_angle'},
])
def test_unqualified_pending_state_cannot_complete_pose(changes):
with fixture() as (collector, cp, sm):
assert type(observe(collector, cp, sm, BASE)) is PendingPhysical
pending_right(collector, sm, **changes)
assert type(observe(collector, cp, sm, BASE + 12_000_000)) is PendingPhysical
def test_pending_state_does_not_replace_current_native_permission():
with fixture() as (collector, cp, sm):
pending_right(collector, sm)
collector._learning_current_native_allowed = False
assert observe(collector, cp, sm, BASE + 12_000_000) is None
def test_conflicting_source_state_is_refused():
with fixture() as (collector, cp, sm):
pending_right(collector, sm)
control, _ = pair(BASE - 10_000_000, speed=99.)
collector.controls[control.control_ns] = control
assert observe(collector, cp, sm, BASE + 12_000_000) is None
@@ -0,0 +1,137 @@
"""Split policy with a declared native verifier fixture; no CAN or road claim."""
from dataclasses import replace
from types import SimpleNamespace
from typing import cast
from unittest.mock import Mock
import pytest
from opendbc.car.steering_projection import TorqueProjection
from openpilot.starpilot.flm.fullrate_collector import FullrateCollector
from openpilot.starpilot.flm.tests.test_fullrate_pipeline import BASE, SyntheticAdapter, snapshots
from openpilot.starpilot.flm.torque_eligibility import consumed_sources_current
from openpilot.starpilot.flm.unchanged_output import redundant_late_output
def fixture():
control, output, _ = snapshots(BASE)
control = replace(control, domain='normalized_torque', native_required=True, native_allowed=True,
native_identity='4' * 64, native_observed_ns=BASE - 50_000_000, native_expires_ns=BASE + 150_000_000)
limits = tuple(zip(('STEER_DELTA_UP', 'STEER_DELTA_DOWN', 'STEER_DRIVER_ALLOWANCE',
'STEER_DRIVER_MULTIPLIER', 'STEER_DRIVER_FACTOR'), (10., 10., 50., 2., 1.), strict=True))
witness = TorqueProjection(1, 'hyundai', 'synthetic', 0, 'hyundai.LKAS11', 'round', 10,
.2, .2, 200, 200, 1000., (), (), 'driver', 200., 0., limits, (), True, 832, 0, b'12345678', .2)
previous = replace(output, domain='normalized_torque', state_ns=BASE + 19_000_000,
apply_ns=BASE + 20_000_000, projection=witness)
later = replace(previous, state_ns=BASE + 29_000_000, apply_ns=BASE + 31_000_000,
projection=witness._replace(frame=11, can_data=b'87654321'))
collector = FullrateCollector(SyntheticAdapter())
collector.adapter.command_domain = 'normalized_torque'
collector.adapter.replay = SimpleNamespace(applied_matches=Mock(return_value=True))
collector.live_tracking = SimpleNamespace(episodes=SimpleNamespace(source_tracking=True), interrupt=Mock())
collector._learning_current_native_allowed = True
collector.ready.append((control, previous))
collector.last_apply = previous.apply_ns
collector.last_response = collector.last_qualified_response = BASE + 21_000_000
return collector, control, previous, later
def test_verified_unchanged_repeat_does_not_erase_or_renew_existing_evidence():
collector, control, previous, later = fixture()
assert consumed_sources_current(control, previous)
assert not consumed_sources_current(control, later)
prior = (tuple(collector.ready), collector.last_apply, collector.last_response, collector.last_qualified_response)
collector._join_emission(control, later)
assert (tuple(collector.ready), collector.last_apply, collector.last_response, collector.last_qualified_response) == prior
collector.live_tracking.interrupt.assert_not_called()
collector.adapter.replay.applied_matches.assert_called_once_with(control, later)
assert not collector.training_interruptions
@pytest.mark.parametrize('case', ('no_prior', 'different_control', 'plant', 'native', 'decode', 'changed_output',
'limited', 'override', 'fault', 'inactive', 'invalid', 'card_age', 'source_age', 'expired_native',
'old_prior', 'codec', 'requested', 'not_late', 'native_disabled'))
def test_repeat_exception_cannot_admit_changed_unqualified_or_unverified_input(case):
collector, control, previous, later = fixture()
if case == 'no_prior':
collector.ready.clear()
elif case == 'different_control':
control = replace(control, generation=control.generation + 1)
elif case == 'plant':
collector.live_tracking.episodes.source_tracking = False
elif case == 'native':
collector._learning_current_native_allowed = False
elif case == 'decode':
collector.adapter.replay.applied_matches.return_value = False
elif case == 'changed_output':
later = replace(later, applied=.201, projection=cast(TorqueProjection, later.projection)._replace(final_raw=201, reported_torque=.201))
elif case in ('limited', 'override', 'fault'):
later = replace(later, **{case: True})
elif case in ('inactive', 'invalid'):
later = replace(later, **{'active' if case == 'inactive' else 'valid': False})
elif case == 'card_age':
later = replace(later, state_ns=BASE)
elif case == 'source_age':
control = replace(control, state_ns=BASE - 30_000_001)
collector.ready[-1] = (control, previous)
elif case == 'expired_native':
control = replace(control, native_expires_ns=BASE + 30_000_000)
collector.ready[-1] = (control, previous)
elif case == 'old_prior':
later = replace(later, apply_ns=BASE + 50_000_001, state_ns=BASE + 50_000_000)
elif case == 'codec':
later = replace(later, projection=cast(TorqueProjection, later.projection)._replace(can_bus=1))
elif case == 'requested':
later = replace(later, projection=cast(TorqueProjection, later.projection)._replace(requested_torque=.201))
elif case == 'not_late':
later = replace(later, apply_ns=BASE + 30_000_000)
elif case == 'native_disabled':
later = replace(later, native_allowed=False)
assert not redundant_late_output(collector, control, later)
def test_failed_native_decode_keeps_existing_withdrawal():
collector, control, _, later = fixture()
collector.adapter.replay.applied_matches.return_value = False
collector._join_emission(control, later)
assert not collector.ready and collector.last_apply == collector.last_qualified_response == 0
collector.live_tracking.interrupt.assert_called_once_with('consumed_source_mismatch', context_changed=False)
@pytest.mark.parametrize('tamper', (False, True))
def test_ioniq6_unchanged_repeat_uses_actual_can_decoder(tmp_path, tamper):
# Physical state/current permission are declared test fixtures. CarInterface,
# encoder, packets, emission history and decoder are the registered path.
from opendbc.car.hyundai.values import CAR
from openpilot.starpilot.flm.controller_replay import ControllerReplay
from openpilot.starpilot.flm.torque_emission_replay import EmissionHistory
from openpilot.starpilot.flm.tests.test_torque_emission_join import interface, cc, settle, emitted, BASE as ENCODER_BASE
ci = interface(CAR.HYUNDAI_IONIQ_6)
command = cc(0.)
settle(ci, command)
stamp = ENCODER_BASE + ci.CC.frame * 10_000_000
source, first = emitted(ci, command, tmp_path, control_ns=stamp)
collector, control, _, _ = fixture()
control = replace(control, requested=0., control_ns=stamp, state_ns=stamp-1,
identity=source.identity, source_epoch=source.source_epoch, baseline_revision=source.baseline_revision,
native_required=False)
history = EmissionHistory(ci.CP, identity=control.identity, baseline_revision=control.baseline_revision)
rows = [first]
for _ in range(4):
_, output = emitted(ci, command, tmp_path, control_ns=stamp)
rows.append(output)
if tamper:
witness = rows[-1].projection
assert witness is not None
data = bytes([witness.can_data[0] ^ 0xff]) + witness.can_data[1:]
rows[-1] = replace(rows[-1], projection=witness._replace(can_data=data))
for output in rows:
assert history.record_carrier(output) and history.record(control, output)
previous, later = rows[-2:]
assert consumed_sources_current(control, previous) and not consumed_sources_current(control, later)
replay = ControllerReplay.__new__(ControllerReplay)
replay.emissions = history
collector.adapter.replay = replay
collector.ready.clear()
collector.ready.append((control, previous))
assert redundant_late_output(collector, control, later) is not tamper
@@ -0,0 +1,42 @@
"""Discard an unchanged repeat without admitting a late actuation sample."""
def redundant_late_output(collector, control, output):
from openpilot.starpilot.flm.fullrate_collector import JOIN_NS
from openpilot.starpilot.flm.torque_eligibility import consumed_sources_current
from opendbc.car.steering_projection import TorqueProjection, projection_to_dict
live = collector.live_tracking
if (live is None or getattr(getattr(live, 'episodes', None), 'source_tracking', False) is not True
or getattr(collector, '_learning_current_native_allowed', False) is not True
or control.domain != 'normalized_torque' or output.domain != control.domain or not collector.ready):
return False
previous_control, previous = collector.ready[-1]
if (previous_control is not control or not consumed_sources_current(control, previous)
or not control.state_ns <= control.control_ns <= output.state_ns <= output.apply_ns
or control.control_ns - control.state_ns > JOIN_NS or output.apply_ns - output.state_ns > JOIN_NS
or output.apply_ns - control.control_ns <= JOIN_NS
or not previous.apply_ns < output.apply_ns <= previous.apply_ns + JOIN_NS
or not control.enabled or not control.active or not control.valid or control.manual or control.override or control.fault
or control.saturated or control.model_wobble
or any(not row.valid or not row.active or row.override or row.fault or row.limited or not row.native_allowed
for row in (previous, output))):
return False
if control.native_required and (not control.native_allowed or
not 0 < control.native_observed_ns <= control.control_ns <= output.apply_ns <= control.native_expires_ns <= control.native_observed_ns + 200_000_000):
return False
old, new = previous.projection, output.projection
if (type(old) is not TorqueProjection or type(new) is not TorqueProjection
or projection_to_dict(old) is None or projection_to_dict(new) is None
or output.control_ns != control.control_ns or output.applied != previous.applied
or new.requested_torque != control.requested or new.reported_torque != output.applied or not new.steer_requested):
return False
# Current CAN bytes/counter and native prestate are independently verified
# below. Every other encoded command/codec field must remain exactly equal.
changing = {'frame', 'can_data', 'previous_torque', 'measured_torque'}
if any(getattr(old, name) != getattr(new, name) for name in TorqueProjection._fields if name not in changing):
return False
replay = getattr(collector.adapter, 'replay', None)
if replay is None:
return False
return replay.applied_matches(control, output) is True
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "99155a256dac423974ec48b2ea94b5b7b8cea2412cd207350f5cf259e238fc55", "schema_version": 1}
{"protected_inventory_sha256": "785627931b899d1f0b0af928e64147556fe0ded7f60d012770fbaf9a6455978b", "schema_version": 1}