Restore Mazda camera stop-sign observations

This commit is contained in:
firestar5683
2026-10-22 14:00:06 -05:00
parent 2985ed9f6d
commit 178c8694f2
6 changed files with 189 additions and 3 deletions
+31 -2
View File
@@ -2,11 +2,29 @@ from opendbc.can import CANDefine, CANParser
from opendbc.car import Bus, create_button_events, structs
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.interfaces import CarStateBase
from opendbc.car.mazda.values import DBC, LKAS_LIMITS
from opendbc.car.mazda.values import CAR, DBC, LKAS_LIMITS, MazdaFlags
from opendbc.car.mazda.stock_aol import qualified
ButtonType = structs.CarState.ButtonEvent.Type
DASHBOARD_CARS = frozenset(CAR)
class DashboardCameraParser(CANParser):
def __init__(self, dbc_name):
super().__init__(dbc_name, [], 2)
self.dashboard_parser = CANParser(dbc_name, [("CAM_TRAFFIC_SIGNS", float('nan'))], 2)
def update(self, strings, sendcan=False):
packets = strings
if packets and not isinstance(packets[0], list | tuple):
packets = [packets]
result = super().update(packets, sendcan)
admitted = [(stamp, [(address, data, bus) for address, data, bus in frames
if address == 0x35F and bus == 2 and len(data) == 8]) for stamp, frames in packets]
self.dashboard_parser.update(admitted, sendcan)
return result
class CarState(CarStateBase):
def __init__(self, CP):
@@ -23,6 +41,7 @@ class CarState(CarStateBase):
self.accel_button = 0
self.decel_button = 0
self.cancel_button = 0
self.dashboard_stop_sign = None
def update(self, can_parsers) -> structs.CarState:
cp = can_parsers[Bus.pt]
@@ -128,11 +147,21 @@ class CarState(CarStateBase):
*create_button_events(self.decel_button, prev_decel_button, {1: ButtonType.decelCruise}),
]
self.dashboard_stop_sign = None
if isinstance(cp_cam, DashboardCameraParser):
dashboard = cp_cam.dashboard_parser
stamp = dashboard.ts_nanos["CAM_TRAFFIC_SIGNS"]["STOP_SIGN"]
if stamp > 0:
self.dashboard_stop_sign = (dashboard.vl["CAM_TRAFFIC_SIGNS"]["STOP_SIGN"] == 9, stamp)
return ret
@staticmethod
def get_can_parsers(CP):
return {
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], 0),
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], 2),
Bus.cam: (DashboardCameraParser(DBC[CP.carFingerprint][Bus.pt])
if CP.brand == 'mazda' and CP.carFingerprint in DASHBOARD_CARS and
int(CP.flags) == int(MazdaFlags.GEN1) and not CP.notCar
else CANParser(DBC[CP.carFingerprint][Bus.pt], [], 2)),
}
@@ -0,0 +1,118 @@
"""Factory/parser/Card regressions for root's coordinated native runtime."""
from types import SimpleNamespace
import pytest
from opendbc.can import CANPacker
from opendbc.car import Bus, gen_empty_fingerprint
from opendbc.car.mazda.interface import CarInterface
from opendbc.car.mazda.values import CAR, DBC
from opendbc.car.mazda.tests.test_stock_startup import complete_frames, exercise_startup
from openpilot.starpilot.car.mazda import dashboard
SUPPORTED = (CAR.MAZDA_CX5_2022, CAR.MAZDA_CX9_2021)
@pytest.mark.parametrize('identity', list(CAR))
@pytest.mark.parametrize('alpha,release', ((False, False), (True, False), (False, True), (True, True)))
def test_all_factory_variants_preserve_stock_ownership(identity, alpha, release):
cp = CarInterface.get_params(identity, gen_empty_fingerprint(), [], alpha, release, False)
assert cp.dashcamOnly is (identity not in SUPPORTED)
assert cp.pcmCruise and not cp.openpilotLongitudinalControl and cp.mass > 1000
assert cp.steerActuatorDelay == pytest.approx(0.1) and cp.steerLimitTimer == pytest.approx(0.8)
assert cp.safetyConfigs[0].safetyParam == 0 and len(cp.safetyConfigs) == 1
assert type(CarInterface(cp).CC).__module__ == 'opendbc.car.mazda.carcontroller'
@pytest.mark.parametrize('identity', SUPPORTED)
@pytest.mark.parametrize('aol', (False, True))
def test_actual_card_composes_original_camera_observation(identity, aol, monkeypatch):
from openpilot.cereal import messaging
def probe(card, ci, controls, saved):
mono, offset = [100_000_000_000], 2_000_000_000
monkeypatch.setattr(dashboard.time, 'monotonic_ns', lambda: mono[0])
monkeypatch.setattr(dashboard.time, 'clock_gettime_ns', lambda clock: mono[0] + offset)
create = messaging.new_message
def new_message(service=None, size=None, **kwargs):
kwargs.setdefault('logMonoTime', mono[0])
return create(service, size, **kwargs)
monkeypatch.setattr(messaging, 'new_message', new_message)
sent, send = [], card.pm.send
def capture(service, event):
if service == 'starpilotCarState':
event = event.to_bytes()
sent.append(messaging.log_from_bytes(event).as_builder())
return send(service, event)
monkeypatch.setattr(card.pm, 'send', capture)
packer, frozen = CANPacker(DBC[identity][Bus.pt]), card.CP.to_dict()
value = 0
for tick in range(1, 80):
mono[0] += 10_000_000
rx = complete_frames(packer, card.CP, tick, main=False)
if 41 <= tick <= 58:
value = (list(range(16)) + [9, 0])[tick - 41]
rx.append((0x35F, bytes((0, 0, 0, value << 4, 0, 0, 0, 0)), 2))
state = ci.update([(mono[0] + offset, rx)])
if tick < 41:
continue
assert state.canValid and not state.canTimeout
sent.clear()
card.state_publish(state, None)
assert len(sent) == 1
event = sent[0]
assert event.valid and event.logMonoTime == mono[0]
payload = event.starpilotCarState
assert payload.dashboardStopSign == int(value == 9)
assert card.CP.to_dict() == frozen == controls.CP.to_dict()
exercise_startup(identity, False, False, aol, monkeypatch, probe=probe)
@pytest.mark.parametrize('bus,length', ((0, 8), (1, 8), (2, 4), (2, 7), (2, 9), (2, 64)))
def test_wrong_bus_and_dlc_cannot_create_camera_evidence(bus, length):
cp = CarInterface.get_params(SUPPORTED[0], gen_empty_fingerprint(), [], False, False, False)
ci = CarInterface(cp)
ci.update([(100_000_000_000, [(0x35F, bytes(length), bus)])])
assert ci.CS.dashboard_stop_sign is None
@pytest.mark.parametrize('age', (-1, 0, 150_000_000, 150_000_001))
@pytest.mark.parametrize('health', ('healthy', 'invalid', 'timeout'))
def test_composition_preserves_other_evidence(age, health, monkeypatch):
from openpilot.cereal import messaging
cp = CarInterface.get_params(SUPPORTED[0], gen_empty_fingerprint(), [], False, False, False)
now, offset = 100_000_000_000, 2_000_000_000
monkeypatch.setattr(dashboard.time, 'monotonic_ns', lambda: now)
monkeypatch.setattr(dashboard.time, 'clock_gettime_ns', lambda clock: now + offset)
message = messaging.new_message('starpilotCarState', valid=True, logMonoTime=now)
message.starpilotCarState.sourceCarStateMonoTime = now - 1
message.starpilotCarState.learningApplication.profileSequence = 7
state = SimpleNamespace(canValid=health != 'invalid', canTimeout=health == 'timeout')
parsed = SimpleNamespace(dashboard_stop_sign=(True, now + offset - age))
assert dashboard.compose(cp, parsed, state, message, car_state_stamp=now) is message
assert message.valid and message.logMonoTime == now
assert message.starpilotCarState.sourceCarStateMonoTime == now - 1
assert message.starpilotCarState.learningApplication.profileSequence == 7
valid = health == 'healthy' and 0 <= age <= 150_000_000
assert message.starpilotCarState.dashboardStopSign == int(valid)
@pytest.mark.parametrize('identity', list(CAR))
def test_all_six_dom_dashboard_identities_observe_without_new_actuation(identity, monkeypatch):
cp = CarInterface.get_params(identity, gen_empty_fingerprint(), [], False, False, False)
ci = CarInterface(cp)
before = cp.to_dict()
now, offset = 100_000_000_000, 2_000_000_000
monkeypatch.setattr(dashboard.time, 'monotonic_ns', lambda: now)
monkeypatch.setattr(dashboard.time, 'clock_gettime_ns', lambda clock: now + offset)
packer = CANPacker(DBC[identity][Bus.pt])
for tick in range(1, 41):
source = now + offset - (40 - tick) * 10_000_000
rx = complete_frames(packer, cp, tick, main=False)
rx.append((0x35F, bytes((0, 0, 0, 0x90, 0, 0, 0, 0)), 2))
state = ci.update([(source, rx)])
assert state.canValid and not state.canTimeout
assert ci.CS.dashboard_stop_sign == (True, now + offset)
message = dashboard.compose(cp, ci.CS, state, None, car_state_stamp=now)
assert message.starpilotCarState.dashboardStopSign == 1
assert cp.to_dict() == before
assert cp.dashcamOnly is (identity not in SUPPORTED)
assert cp.pcmCruise and not cp.openpilotLongitudinalControl
+3
View File
@@ -1054,6 +1054,9 @@ class Car:
companion = feedback_companion
except Exception:
self.learning_feedback_publication = None
if self.CP.brand == 'mazda':
from openpilot.starpilot.car.mazda.dashboard import compose
companion = compose(self.CP, self.CI.CS, CS, companion, car_state_stamp=int(cs_send.logMonoTime))
self.car_gps_publisher.update(self.CI.CS, self.pm, companion)
self.pm.send('carState', cs_send)
if getattr(self, 'learning_stamp_observer', None) is not None:
@@ -0,0 +1,35 @@
"""Optional Mazda camera observation; never a longitudinal control grant."""
import time
from opendbc.car.mazda.values import CAR, MazdaFlags
from openpilot.cereal import messaging
MAX_AGE_NS = 150_000_000
DASHBOARD_CARS = frozenset(CAR)
def capable(cp):
# Read-only observation scope is separate from active-control/AOL admission.
# Dom's intentional dashboard commit explicitly covers all six GEN1 IDs.
return (cp.brand == 'mazda' and cp.carFingerprint in DASHBOARD_CARS and int(cp.flags) == int(MazdaFlags.GEN1) and not cp.notCar)
def compose(cp, parsed, state, message, *, car_state_stamp):
if not capable(cp):
return message
if message is None:
message = messaging.new_message('starpilotCarState', valid=bool(state.canValid), logMonoTime=car_state_stamp)
message.starpilotCarState.sourceCarStateMonoTime = car_state_stamp
# Missing or stale optional camera data must not invalidate unrelated evidence.
message.starpilotCarState.dashboardStopSign = 0
before = time.monotonic_ns()
boot = time.clock_gettime_ns(getattr(time, 'CLOCK_BOOTTIME', time.CLOCK_MONOTONIC))
after = time.monotonic_ns()
observation = getattr(parsed, 'dashboard_stop_sign', None)
if (type(observation) is tuple and len(observation) == 2 and type(observation[0]) is bool and
type(observation[1]) is int and 0 <= after - before <= 1_000_000 and state.canValid and not state.canTimeout):
age = boot - observation[1]
stamp = before - age
if 0 <= age <= MAX_AGE_NS and 0 < stamp <= car_state_stamp:
message.starpilotCarState.dashboardStopSign = int(observation[0])
return message
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "66c65be741a8b1e5726aab053897f5bea235dd48697dfd7ec7f908cbb2dee514", "schema_version": 1}
{"protected_inventory_sha256": "68477cd60a2523b67edcbdb8eabdadc32b3e686aa3e4429987b40b903a570322", "schema_version": 1}
+1
View File
@@ -41,6 +41,7 @@ AOL_LIFECYCLE_TARGETS = (
"openpilot/starpilot/aol/tests/test_honda_recovery_lifecycle.py",
"openpilot/starpilot/aol/tests/test_fresh_inactive_transport.py",
"opendbc_repo/opendbc/car/mazda/tests/test_native_engagement_callers.py",
"opendbc_repo/opendbc/car/mazda/tests/test_dashboard_stop_sign.py",
"opendbc_repo/opendbc/safety/tests/test_mazda_stock_aol.py",
"openpilot/starpilot/aol/tests/test_temporary_lag_intent.py",
"openpilot/starpilot/aol/tests/test_stock_location_recovery.py",