Files
StarPilot/openpilot/starpilot/tests/test_tesla_host_integration.py
T
2026-11-02 12:28:46 -06:00

240 lines
13 KiB
Python

"""Synthetic host -> native carControl IPC -> Tesla CAN -> DEBUG C safety.
Supplied host enable/override inputs are not a mode policy or rearm test. Vehicle
state and parser health come from scheduled CAN. The published command reaches
the interface unchanged; no vehicle, EPS actuation or fleet qualification follows.
"""
import argparse
import ctypes
import importlib
import importlib.machinery
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import time
import unittest
from openpilot.common.basedir import BASEDIR
FAKE_FLAGS = ("CEREAL_FAKE", "CEREAL_FAKE_PREFIX", "SIMULATION")
PHASES = (
("active", True, False, 1.0),
("supplied_longitudinal_override", True, True, 1.0),
("active_before_disable", True, False, 1.0),
("disabled_positive_plan", False, False, 1.0),
("disabled_negative_plan", False, False, -2.0),
("explicitly_reenabled_host_input", True, False, 1.0),
)
def probe(profile, output):
# Native IPC libraries read process-global environment: validate before importing them.
prefix = os.environ.get("OPENPILOT_PREFIX", "")
if not prefix.startswith("tesla-host-") or not os.environ.get("PARAMS_ROOT") or any(name in os.environ for name in FAKE_FLAGS):
raise RuntimeError("An isolated native Tesla host test environment is required")
from openpilot.cereal import messaging
from openpilot.common import params as params_module
from openpilot.common.params import Params
from openpilot.selfdrive.controls.controlsd import Controls
from openpilot.starpilot.tests.tesla_fixture import EPS, FW_VERSIONS, NativeSafety, TeslaFixture, sha256, sha256_bytes
native = {}
for name in ("capnp.lib.capnp", "msgq._ipc", "_cffi_backend"):
module_file = importlib.import_module(name).__file__
if module_file is None:
raise RuntimeError(f"Native extension has no file: {name}")
path = Path(module_file).resolve()
if not any(str(path).endswith(suffix) for suffix in importlib.machinery.EXTENSION_SUFFIXES):
raise RuntimeError(f"Tesla host integration requires a native extension: {name}")
native[name] = {"path": str(path), "sha256": sha256(path)}
if not isinstance(params_module.lib, ctypes.CDLL):
raise RuntimeError("Tesla host integration requires native Params")
library = Path(params_module.lib._name).resolve()
native["Params C ABI"] = {"path": str(library), "sha256": sha256(library)}
safety = NativeSafety()
observations = []
try:
fixture = TeslaFixture(safety, profile=profile, longitudinal=True)
primed = fixture.prime()
params = Params()
cp_bytes = fixture.cp.to_bytes()
params.put("CarParams", cp_bytes, block=True)
controls = Controls()
subscriber = messaging.sub_sock("carControl", timeout=1000)
for phase, enabled, override, target in PHASES:
for _ in range(20):
host_observation = {}
def published_control(state, timestamp, phase=phase, enabled=enabled, override=override, target=target,
host_observation=host_observation):
inputs = []
def message(service, size=None):
msg = messaging.new_message(service, size, valid=True, logMonoTime=timestamp)
inputs.append(msg)
return getattr(msg, service)
car_state = messaging.new_message("carState", valid=state.canValid, logMonoTime=timestamp)
car_state.carState = state # Actual parser result: do not overwrite health, speed, pedals or cruise.
inputs.append(car_state)
host = message("selfdriveState")
host.enabled = host.active = enabled
host.state = "enabled" if enabled else "disabled"
parameters = message("vehicleParameters")
parameters.stiffnessFactor, parameters.steerRatio = 1.0, fixture.cp.steerRatio
message("longitudinalPlan").aTarget = target
message("modelV2").action.desiredCurvature = 0.0001
message("lateralDelay").lateralDelay = 0.1
message("carOutput").actuatorsOutput = fixture.last_actuators
events = message("onroadEvents", 1 if override else 0)
if override:
events[0].name = "gasPressedOverride"
events[0].overrideLongitudinal = True
controls.sm.update_msgs(timestamp / 1e9, [msg.as_reader() for msg in inputs])
prior_acceleration = float(controls.LoC.last_output_accel)
command, lateral_log = controls.state_control()
publication_begin = int(time.monotonic() * 1e9)
controls.publish(command, lateral_log)
received = messaging.recv_one(subscriber)
publication_end = int(time.monotonic() * 1e9)
if received is None:
raise RuntimeError("Native carControl publication was not received")
emitted = received.carControl
host_observation.update({"phase": phase, "plan_acceleration": target,
"supplied_enabled": enabled, "supplied_override": override,
"logical_frame_nanos": timestamp,
"publication_begin": publication_begin, "publication_end": publication_end,
"publication_log_mono_time": received.logMonoTime,
"publication_valid": received.valid,
"prior_host_acceleration": prior_acceleration,
"host_published_command": command.to_dict(),
"car_control_sha256": sha256_bytes(emitted.as_builder().to_bytes()),
"car_control": emitted.to_dict(), "parsed_car_state": state.to_dict(),
"host_car_state": controls.sm["carState"].to_dict()})
return emitted # Pass this actual IPC reader to interface.apply; no reconstructed command.
record = fixture.step(control_callback=published_control)
observations.append({"host": host_observation, "vehicle": record})
platform = fixture.cp.carFingerprint
firmware = FW_VERSIONS[platform][EPS][0]
paths = ("openpilot/selfdrive/controls/controlsd.py", "openpilot/selfdrive/controls/lib/longcontrol.py",
"openpilot/starpilot/tests/tesla_fixture.py", "openpilot/starpilot/tests/test_tesla_host_integration.py",
"opendbc_repo/opendbc/car/tesla/interface.py", "opendbc_repo/opendbc/car/tesla/carcontroller.py",
"opendbc_repo/opendbc/car/tesla/teslacan.py", "opendbc_repo/opendbc/car/tesla/carstate.py")
report = {"scope": "synthetic host state_control/publish through native IPC, vehicle interface/controller, CAN and DEBUG C safety",
"vehicle_qualified": False, "product_mode_qualified": False, "firmware_profile": profile,
"platform": platform, "input_eps_firmware_hex": firmware.hex(), "flags": fixture.cp.flags,
"safety_param": fixture.cp.safetyConfigs[0].safetyParam,
"car_params_sha256": sha256_bytes(cp_bytes), "native": native, "safety_build": safety.provenance,
"isolation": {"params_root": os.environ["PARAMS_ROOT"], "ipc_prefix": prefix,
"forbidden_flags_present": [name for name in FAKE_FLAGS if name in os.environ]},
"sources": {path: sha256(Path(BASEDIR) / path) for path in paths},
"primed": primed, "observations": observations}
Path(output).write_text(json.dumps(report, indent=2) + "\n")
finally:
safety.close()
class TestTeslaHostIntegration(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.reports = []
def check_profile(self, profile):
ipc_root = "/tmp" if sys.platform == "darwin" else "/dev/shm"
with tempfile.TemporaryDirectory() as temporary, tempfile.TemporaryDirectory(prefix="msgq_tesla-host-", dir=ipc_root) as ipc:
output = Path(temporary) / "observations.json"
environment = dict(os.environ, PARAMS_ROOT=str(Path(temporary) / "params"),
OPENPILOT_PREFIX=Path(ipc).name.removeprefix("msgq_"))
# Exercise all inherited-flag removals; the child independently rejects any that survive.
environment.update(dict.fromkeys(FAKE_FLAGS, "must-not-be-used"))
for name in FAKE_FLAGS:
environment.pop(name, None)
command = [sys.executable, "-m", "openpilot.starpilot.tests.test_tesla_host_integration", "--probe",
"--profile", profile, "--output", str(output)]
completed = subprocess.run(command, cwd=BASEDIR, env=environment, capture_output=True, text=True, timeout=30)
self.assertEqual(completed.returncode, 0, completed.stdout + completed.stderr)
report = json.loads(output.read_text())
self.reports.append(report)
self.assertEqual(report["isolation"]["forbidden_flags_present"], [])
self.assertEqual(len(report["observations"]), 120)
self.assertEqual([r["vehicle"]["frame"] for r in report["observations"]], list(range(30, 150)))
self.assertEqual([r["host"]["phase"] for r in report["observations"]], [p[0] for p in PHASES for _ in range(20)])
self.assertTrue(report["primed"]["can_valid"])
self.assertTrue(report["primed"]["safety_config_valid"])
for phase, enabled, override, _ in PHASES:
rows = [row for row in report["observations"] if row["host"]["phase"] == phase]
self.assertEqual(len(rows), 20)
if override or phase == "disabled_positive_plan":
self.assertGreater(rows[0]["host"]["prior_host_acceleration"], 0)
steering, longitudinal = [], []
for row in rows:
with self.subTest(profile=profile, phase=phase, frame=row["vehicle"]["frame"]):
host, vehicle = row["host"], row["vehicle"]
cc = host["car_control"]
self.assertEqual(host["car_control_sha256"], vehicle["car_control_sha256"])
self.assertEqual(host["host_published_command"], cc)
self.assertEqual(host["parsed_car_state"], host["host_car_state"])
self.assertEqual(host["logical_frame_nanos"], vehicle["nanos"])
self.assertGreaterEqual(host["publication_log_mono_time"], host["publication_begin"])
self.assertLessEqual(host["publication_log_mono_time"], host["publication_end"])
self.assertTrue(host["publication_valid"])
self.assertTrue(vehicle["can_valid"])
self.assertFalse(vehicle["can_timeout"])
self.assertTrue(vehicle["safety_config_valid"])
self.assertTrue(all(vehicle["rx_accepted"]))
self.assertEqual(cc["enabled"], enabled)
self.assertEqual(cc["latActive"], enabled)
self.assertEqual(cc["longActive"], enabled and not override)
self.assertEqual(cc["cruiseControl"]["cancel"], not enabled)
self.assertEqual(cc["cruiseControl"]["override"], override)
if enabled and not override:
self.assertGreater(cc["actuators"]["accel"], 0)
else:
self.assertEqual(cc["actuators"]["accel"], 0)
for emitted in vehicle["outgoing"]:
self.assertTrue(emitted["decoded_fresh"])
self.assertTrue(emitted["safety_tx_accepted"])
if emitted["address"] == 0x488:
steering.append(emitted)
self.assertEqual(emitted["signals"]["DAS_steeringControlType"], 1 if enabled else 0)
elif emitted["address"] == 0x2B9:
longitudinal.append(emitted)
signals = emitted["signals"]
self.assertEqual(signals["DAS_accState"], 4 if enabled else 13)
self.assertAlmostEqual(signals["DAS_accelMin"], cc["actuators"]["accel"], delta=0.021)
self.assertAlmostEqual(signals["DAS_accelMax"], cc["actuators"]["accel"], delta=0.021)
self.assertEqual(len(steering), 10)
self.assertEqual(len(longitudinal), 5)
for emitted, counter_name, modulus in ((steering, "DAS_steeringControlCounter", 16),
(longitudinal, "DAS_controlCounter", 8)):
counters = [message["signals"][counter_name] for message in emitted]
self.assertEqual(counters[1:], [(value + 1) % modulus for value in counters[:-1]])
# The sequence must remain continuous across host phase boundaries as well.
for address, counter_name, modulus in ((0x488, "DAS_steeringControlCounter", 16), (0x2B9, "DAS_controlCounter", 8)):
counters = [message["signals"][counter_name] for row in report["observations"] for message in row["vehicle"]["outgoing"]
if message["address"] == address]
self.assertEqual(counters[1:], [(value + 1) % modulus for value in counters[:-1]])
def test_marker489_host_command_reaches_can_unchanged(self):
self.check_profile("marker489")
def test_marker054_host_command_reaches_can_unchanged(self):
self.check_profile("marker054")
if __name__ == "__main__":
if "--probe" in sys.argv:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--probe", action="store_true")
parser.add_argument("--profile", choices=("marker489", "marker054"), required=True)
parser.add_argument("--output", type=Path, required=True)
arguments = parser.parse_args()
probe(arguments.profile, arguments.output)
else:
unittest.main()