mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
80fbb4f6db
Register vehicle controls, planner modes, UI actions and Galaxy services after their providers exist. Wire the shared runtime entrypoints and cover their combined behavior. Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com> Co-authored-by: Daniel Koepping <elkoled@gmail.com> Co-authored-by: Danny <dannyirwin31@yahoo.com> Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com> Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com> Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com> Co-authored-by: Kacper Rączy <gfw.kra@gmail.com> Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu> Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com> Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Co-authored-by: stef <19478336+stefpi@users.noreply.github.com> Co-authored-by: whoisdomi <dcantu83@gmail.com> Co-authored-by: Intelli <contact@intelli.software> Co-authored-by: haraschax <6804392+haraschax@users.noreply.github.com> Co-authored-by: prabhaavp <prabhaav@msu.edu>
240 lines
13 KiB
Python
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_pyx", "_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()
|