mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
194 lines
9.5 KiB
Python
194 lines
9.5 KiB
Python
"""Native upstream host neutralization under supplied state, not mode qualification.
|
|
|
|
The input messages are synthetic, explicitly supplied through SubMaster.update_msgs.
|
|
The real Controls constructor, longitudinal controller, publication and native IPC
|
|
run in a fresh child process. No manager, vehicle, policy coordinator or CAN runs.
|
|
"""
|
|
import argparse
|
|
import ctypes
|
|
import hashlib
|
|
import importlib
|
|
import importlib.machinery
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from openpilot.common.basedir import BASEDIR
|
|
|
|
|
|
def probe(system_longitudinal, output):
|
|
# Check isolation before importing libraries which own process-global IPC state.
|
|
prefix = os.environ.get("OPENPILOT_PREFIX", "")
|
|
if not prefix.startswith("host-boundary-") or not os.environ.get("PARAMS_ROOT") or "CEREAL_FAKE" in os.environ:
|
|
raise RuntimeError("An isolated native host-boundary test environment is required")
|
|
from opendbc.car import gen_empty_fingerprint
|
|
from opendbc.car.tesla.interface import CarInterface
|
|
from opendbc.car.tesla.values import CAR
|
|
from openpilot.cereal import messaging
|
|
from openpilot.common.params import Params
|
|
from openpilot.common import params as params_module
|
|
from openpilot.selfdrive.controls.controlsd import Controls
|
|
|
|
native = {}
|
|
for name in ("capnp.lib.capnp", "msgq.ipc_pyx"):
|
|
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"Host boundary requires a native extension: {name}")
|
|
native[name] = {"path": str(path), "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}
|
|
if not isinstance(params_module.lib, ctypes.CDLL):
|
|
raise RuntimeError("Host boundary requires native Params")
|
|
params_library = Path(params_module.lib._name).resolve()
|
|
native["Params C ABI"] = {"path": str(params_library), "sha256": hashlib.sha256(params_library.read_bytes()).hexdigest()}
|
|
params = Params()
|
|
fingerprint = gen_empty_fingerprint()
|
|
fingerprint[2][0x293] = 8
|
|
cp = CarInterface.get_params(CAR.TESLA_MODEL_Y, fingerprint, [], system_longitudinal, False, False)
|
|
car_params_bytes = cp.to_bytes()
|
|
params.put("CarParams", car_params_bytes, block=True)
|
|
controls = Controls()
|
|
subscriber = messaging.sub_sock("carControl", timeout=1000)
|
|
observations = []
|
|
phases = (
|
|
("active", True, True, False, False, 1.0),
|
|
("longitudinal_override", True, True, True, False, 1.0),
|
|
("lateral_fault", True, True, False, True, 1.0),
|
|
("disabled", False, False, False, False, 1.0),
|
|
("disabled_negative_plan", False, False, False, False, -2.0),
|
|
("explicitly_reenabled", True, True, False, False, 1.0),
|
|
)
|
|
frame = 0
|
|
for name, enabled, active, override, steering_fault, target in phases:
|
|
for _ in range(12):
|
|
timestamp = 1_000_000_000 + frame * 10_000_000
|
|
frame += 1
|
|
messages = []
|
|
|
|
def message(service, size=None, timestamp=timestamp, messages=messages):
|
|
msg = messaging.new_message(service, size, valid=True, logMonoTime=timestamp)
|
|
messages.append(msg)
|
|
return getattr(msg, service)
|
|
|
|
state = message("carState")
|
|
state.canValid = True # supplied synthetic host input, not observed bus health
|
|
state.vEgo = state.vEgoRaw = 10.0
|
|
state.vCruise = state.vCruiseCluster = 100.0
|
|
state.steeringAngleDeg = 0.0
|
|
state.steerFaultTemporary = steering_fault
|
|
state.cruiseState.enabled = True
|
|
state.cruiseState.available = True
|
|
state.gearShifter = "drive"
|
|
state.seatbeltUnlatched = False
|
|
host = message("selfdriveState")
|
|
host.enabled, host.active = enabled, active
|
|
host.state = "enabled" if enabled else "disabled"
|
|
parameters = message("vehicleParameters")
|
|
parameters.stiffnessFactor = 1.0
|
|
parameters.steerRatio = cp.steerRatio
|
|
message("longitudinalPlan").aTarget = target
|
|
message("modelV2").action.desiredCurvature = 0.0001
|
|
message("lateralDelay").lateralDelay = 0.1
|
|
message("carOutput")
|
|
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 messages])
|
|
command, lateral_log = controls.state_control()
|
|
controls.publish(command, lateral_log)
|
|
received = messaging.recv_one(subscriber)
|
|
if received is None:
|
|
raise RuntimeError("Native carControl publication was not received")
|
|
emitted = received.carControl
|
|
observations.append({"phase": name, "frame": frame, "plan_acceleration": target,
|
|
"enabled": emitted.enabled, "lateral": emitted.latActive, "longitudinal": emitted.longActive,
|
|
"acceleration": emitted.actuators.accel, "steering_angle": emitted.actuators.steeringAngleDeg,
|
|
"cancel": emitted.cruiseControl.cancel, "override": emitted.cruiseControl.override,
|
|
"valid": received.valid})
|
|
source_paths = ("openpilot/selfdrive/controls/controlsd.py", "openpilot/selfdrive/controls/lib/longcontrol.py",
|
|
"openpilot/starpilot/tests/test_control_host_boundary.py")
|
|
report = {"scope": "synthetic upstream host neutralization and native publication only",
|
|
"vehicle_qualification": "not_established", "system_longitudinal": system_longitudinal,
|
|
"identity": "Model Y candidate deliberately supplied; EPS firmware and physical hardware not identified",
|
|
"native": native,
|
|
"car_params_sha256": hashlib.sha256(car_params_bytes).hexdigest(),
|
|
"sources": {path: hashlib.sha256((Path(BASEDIR) / path).read_bytes()).hexdigest() for path in source_paths},
|
|
"observations": observations}
|
|
Path(output).write_text(json.dumps(report, indent=2) + "\n")
|
|
|
|
|
|
class TestControlHostBoundary(unittest.TestCase):
|
|
def check_profile(self, system_longitudinal):
|
|
ipc_root = "/tmp" if sys.platform == "darwin" else "/dev/shm"
|
|
with tempfile.TemporaryDirectory() as temporary, tempfile.TemporaryDirectory(prefix="msgq_host-boundary-", 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_"))
|
|
for name in ("CEREAL_FAKE", "CEREAL_FAKE_PREFIX", "SIMULATION"):
|
|
environment.pop(name, None)
|
|
command = [sys.executable, "-m", "openpilot.starpilot.tests.test_control_host_boundary", "--probe",
|
|
"--owner", "system" if system_longitudinal else "stock", "--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.assertEqual(len(report["observations"]), 72)
|
|
for row in report["observations"]:
|
|
with self.subTest(phase=row["phase"], frame=row["frame"]):
|
|
disabled = row["phase"].startswith("disabled")
|
|
longitudinal = system_longitudinal and row["phase"] not in ("longitudinal_override", "disabled", "disabled_negative_plan")
|
|
self.assertTrue(row["valid"])
|
|
self.assertEqual(row["enabled"], not disabled)
|
|
self.assertEqual(row["lateral"], not disabled and row["phase"] != "lateral_fault")
|
|
self.assertEqual(row["longitudinal"], longitudinal)
|
|
self.assertEqual(row["cancel"], disabled)
|
|
self.assertEqual(row["override"], system_longitudinal and row["phase"] == "longitudinal_override")
|
|
if longitudinal:
|
|
self.assertGreater(row["acceleration"], 0.0)
|
|
else:
|
|
self.assertEqual(row["acceleration"], 0.0)
|
|
|
|
def test_controls_import_does_not_load_model_execution(self):
|
|
program = """
|
|
import importlib.abc
|
|
import sys
|
|
|
|
class NoModelExecution(importlib.abc.MetaPathFinder):
|
|
def find_spec(self, fullname, path=None, target=None):
|
|
if fullname == 'openpilot.selfdrive.modeld.modeld' or fullname == 'tinygrad' or fullname.startswith('tinygrad.'):
|
|
raise AssertionError('Controls must not import model execution: ' + fullname)
|
|
return None
|
|
|
|
sys.meta_path.insert(0, NoModelExecution())
|
|
from openpilot.selfdrive.controls.controlsd import LAT_SMOOTH_SECONDS
|
|
assert type(LAT_SMOOTH_SECONDS) is float and LAT_SMOOTH_SECONDS == 0.0
|
|
"""
|
|
completed = subprocess.run([sys.executable, '-B', '-c', program], cwd=BASEDIR,
|
|
capture_output=True, text=True, timeout=30)
|
|
self.assertEqual(completed.returncode, 0, completed.stdout + completed.stderr)
|
|
|
|
def test_inactive_system_longitudinal_neutralizes_warmed_controller_and_plan(self):
|
|
self.check_profile(True)
|
|
|
|
def test_stock_longitudinal_never_gets_host_acceleration(self):
|
|
with patch.dict(os.environ, {"CEREAL_FAKE": "1", "CEREAL_FAKE_PREFIX": "must-not-be-used"}):
|
|
self.check_profile(False)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if "--probe" in sys.argv:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--probe", action="store_true")
|
|
parser.add_argument("--owner", choices=("stock", "system"), required=True)
|
|
parser.add_argument("--output", type=Path, required=True)
|
|
arguments = parser.parse_args()
|
|
probe(arguments.owner == "system", arguments.output)
|
|
else:
|
|
unittest.main()
|