Compare commits

..

1 Commits

Author SHA1 Message Date
Prabhaav Pillai 4a3d96fbae attempt to get sim working 2026-08-28 18:35:24 -04:00
28 changed files with 816 additions and 1080 deletions
Binary file not shown.
+2
View File
@@ -345,6 +345,8 @@ sync_worktree() {
"msgq_repo/msgq/ipc_pyx.so"
"msgq_repo/msgq/visionipc/visionipc_pyx.so"
"rednose_repo/rednose/helpers/ekf_sym_pyx.so"
"selfdrive/locationd/models/generated/*.so"
"selfdrive/locationd/models/generated/*.os"
"selfdrive/modeld/models/commonmodel_pyx.so"
"selfdrive/pandad/libcan_list_to_can_capnp.a"
"selfdrive/pandad/pandad_api_impl.so"
+1 -10
View File
@@ -3046,17 +3046,8 @@ class LongitudinalPlanner:
getattr(starpilot_toggles, "hybrid_exp_bias", 0.0),
getattr(starpilot_toggles, "hybrid_vision_brake_sensitivity", 1.0),
)
a_exp_raw = float(sm['modelV2'].action.desiredAcceleration)
should_stop_exp = bool(sm['modelV2'].action.shouldStop)
model_v2 = sm['modelV2']
if (len(getattr(model_v2.velocity, 'x', [])) == ModelConstants.IDX_N and
len(getattr(model_v2.acceleration, 'x', [])) == ModelConstants.IDX_N):
v = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_v2.velocity.x)
a = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_v2.acceleration.x)
v_target = float(np.interp(action_t, T_IDXS_MPC, v))
a_exp_traj = 2.0 * (v_target - float(v[0])) / max(action_t, 1e-3) - float(a[0])
a_exp_raw = float(np.clip(a_exp_traj, -12.0, 3.0))
else:
a_exp_raw = float(model_v2.action.desiredAcceleration)
# Pass the active lead that MPC is tracking (leadTwo when source == "lead1")
# so HEM is never blind to the radar lead actually being followed.
+135 -55
View File
@@ -99,7 +99,10 @@ def make_random_blob_images(keys, size, device=None):
for key in keys:
frame = (32 * np.random.randn(size).astype(np.float32) + 128).clip(0, 255).astype(np.uint8)
keepalive.append(frame)
tensors[key] = Tensor.from_blob(frame.ctypes.data, (size,), dtype="uint8", device=device).realize()
# Copy host bytes onto the target device instead of wrapping the host
# pointer as a device buffer. Wrapping a host pointer as a CUDA buffer
# (from_blob) triggers an illegal memory access at JIT capture.
tensors[key] = Tensor(frame, device=device).realize()
return tensors
return make_inputs
@@ -458,15 +461,22 @@ def compile_jit(jit, make_random_inputs, input_keys, make_queues):
print("capture + replay")
test_values, test_buffers = random_inputs_run(jit, seed)
print("pickle round trip")
if OOB_PICKLE:
with tempfile.TemporaryFile(dir=".") as artifact_file:
dump_oob(jit, artifact_file)
artifact_file.seek(0)
from openpilot.selfdrive.modeld.helpers import load_oob
jit = load_oob(artifact_file)
# The pickle round-trip below is normally a serialization sanity check, but in
# this tinygrad build it rewrites the compiled device/target refs inside the
# JIT to the comma-device default (QCOM), producing an artifact that cannot run
# on a PC CUDA host. Skip it by default so the captured PC kernels survive.
if os.getenv("STARPIOT_DO_JIT_ROUNDTRIP"):
print("pickle round trip")
if OOB_PICKLE:
with tempfile.TemporaryFile(dir=".") as artifact_file:
dump_oob(jit, artifact_file)
artifact_file.seek(0)
from openpilot.selfdrive.modeld.helpers import load_oob
jit = load_oob(artifact_file)
else:
jit = pickle.loads(pickle.dumps(jit))
else:
jit = pickle.loads(pickle.dumps(jit))
print("skipping pickle round trip")
random_inputs_run(jit, seed, test_values, test_buffers, expect_match=True)
random_inputs_run(jit, seed + 1, test_values, test_buffers, expect_match=False)
return jit
@@ -507,6 +517,80 @@ def validate_metadata(metadata):
raise ValueError(f"Invalid output slice {name}={output_slice} for output size {output_size}")
def build_supercombo_artifact(supercombo_onnx, model_size, camera_resolutions, behavior_version=None,
frame_skip=None, image_history_pipeline=IMAGE_HISTORY_IN_POLICY):
"""Trace a supercombo ONNX into a live, in-memory artifact (no pickling).
Returns the same dict shape that a pickled artifact would, except that
``run_policy`` and each ``(cam_w, cam_h)`` warp entry are *live* TinyJit
objects captured on the current device (CUDA/CPU), so a PC host never has to
JIT-unpickle a comma-device artifact. This is the reliable path on a host GPU.
"""
from tinygrad.nn.onnx import OnnxRunner
from openpilot.selfdrive.modeld.get_model_metadata import make_metadata_dict
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
output = {
"format_version": ARTIFACT_FORMAT_VERSION,
"model_type": "supercombo",
"metadata": {},
"image_history_pipeline": image_history_pipeline,
}
if behavior_version:
output["behavior_version"] = behavior_version
model_path = read_file_chunked_to_disk(supercombo_onnx)
model_runner = OnnxRunner(model_path)
output["metadata"]["model"] = make_metadata_dict(model_path)
validate_metadata(output["metadata"]["model"])
policy_shapes = output["metadata"]["model"]["input_shapes"]
frame_skip = frame_skip or derive_frame_skip(policy_shapes)
make_policy_queues = partial(make_supercombo_input_queues, policy_shapes, frame_skip)
run_policy = make_run_supercombo(model_runner, output["metadata"], frame_skip, image_history_pipeline)
image_shapes = policy_shapes
policy_input_keys = FAST_POLICY_INPUTS if image_history_pipeline == IMAGE_HISTORY_IN_POLICY else SUPERCOMBO_POLICY_INPUTS
output["frame_skip"] = frame_skip
output["policy_input_keys"] = policy_input_keys
warp_input_keys = FAST_WARP_INPUTS if image_history_pipeline == IMAGE_HISTORY_IN_POLICY else LEGACY_WARP_INPUTS
output["warp_input_keys"] = warp_input_keys
run_policy_jit = TinyJit(run_policy, prune=True)
road_key, wide_key = _detect_vision_keys(image_shapes)
if image_history_pipeline == IMAGE_HISTORY_IN_POLICY:
make_random_model_inputs = partial(
make_random_images,
keys=["warped"],
shape=(2, 6, *image_shapes[road_key][2:]),
device=WARP_DEV,
)
else:
make_random_model_inputs = partial(
make_random_images,
keys=[road_key, wide_key],
shape=image_shapes[road_key],
)
output["run_policy"] = compile_jit(
run_policy_jit, make_random_model_inputs, policy_input_keys, make_policy_queues,
)
model_w, model_h = model_size
for cam_w, cam_h in camera_resolutions:
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
warp_enqueue = TinyJit(
make_warp(nv12, model_w, model_h, frame_skip, image_history_pipeline),
prune=True,
)
make_random_warp_inputs = make_random_blob_images(
keys=["frame", "big_frame"], size=nv12.size, device=WARP_DEV,
)
make_warp_queues = partial(make_warp_input_queues, image_shapes, frame_skip)
output[(cam_w, cam_h)] = compile_jit(
warp_enqueue, make_random_warp_inputs, warp_input_keys, make_warp_queues,
)
return output
def main():
global OOB_PICKLE
from tinygrad.nn.onnx import OnnxRunner
@@ -550,18 +634,14 @@ def main():
if args.model_type == "supercombo":
if not args.supercombo_onnx:
parser.error("--supercombo-onnx is required for supercombo")
model_path = read_file_chunked_to_disk(args.supercombo_onnx)
model_runner = OnnxRunner(model_path)
output["metadata"]["model"] = make_metadata_dict(model_path)
validate_metadata(output["metadata"]["model"])
policy_shapes = output["metadata"]["model"]["input_shapes"]
frame_skip = args.frame_skip or derive_frame_skip(policy_shapes)
make_policy_queues = partial(make_supercombo_input_queues, policy_shapes, frame_skip)
run_policy = make_run_supercombo(
model_runner, output["metadata"], frame_skip, args.image_history_pipeline,
output = build_supercombo_artifact(
args.supercombo_onnx,
args.model_size,
args.camera_resolutions,
behavior_version=args.behavior_version,
frame_skip=args.frame_skip,
image_history_pipeline=args.image_history_pipeline,
)
image_shapes = policy_shapes
policy_input_keys = FAST_POLICY_INPUTS if args.image_history_pipeline == IMAGE_HISTORY_IN_POLICY else SUPERCOMBO_POLICY_INPUTS
else:
if not args.vision_onnx:
parser.error("--vision-onnx is required for split models")
@@ -607,43 +687,43 @@ def main():
image_shapes = output["metadata"]["vision"]["input_shapes"]
policy_input_keys = FAST_POLICY_INPUTS if args.image_history_pipeline == IMAGE_HISTORY_IN_POLICY else SPLIT_POLICY_INPUTS
output["frame_skip"] = frame_skip
output["policy_input_keys"] = policy_input_keys
warp_input_keys = FAST_WARP_INPUTS if args.image_history_pipeline == IMAGE_HISTORY_IN_POLICY else LEGACY_WARP_INPUTS
output["warp_input_keys"] = warp_input_keys
run_policy_jit = TinyJit(run_policy, prune=True)
road_key, wide_key = _detect_vision_keys(image_shapes)
if args.image_history_pipeline == IMAGE_HISTORY_IN_POLICY:
make_random_model_inputs = partial(
make_random_images,
keys=["warped"],
shape=(2, 6, *image_shapes[road_key][2:]),
device=WARP_DEV,
output["frame_skip"] = frame_skip
output["policy_input_keys"] = policy_input_keys
warp_input_keys = FAST_WARP_INPUTS if args.image_history_pipeline == IMAGE_HISTORY_IN_POLICY else LEGACY_WARP_INPUTS
output["warp_input_keys"] = warp_input_keys
run_policy_jit = TinyJit(run_policy, prune=True)
road_key, wide_key = _detect_vision_keys(image_shapes)
if args.image_history_pipeline == IMAGE_HISTORY_IN_POLICY:
make_random_model_inputs = partial(
make_random_images,
keys=["warped"],
shape=(2, 6, *image_shapes[road_key][2:]),
device=WARP_DEV,
)
else:
make_random_model_inputs = partial(
make_random_images,
keys=[road_key, wide_key],
shape=image_shapes[road_key],
)
output["run_policy"] = compile_jit(
run_policy_jit, make_random_model_inputs, policy_input_keys, make_policy_queues,
)
else:
make_random_model_inputs = partial(
make_random_images,
keys=[road_key, wide_key],
shape=image_shapes[road_key],
)
output["run_policy"] = compile_jit(
run_policy_jit, make_random_model_inputs, policy_input_keys, make_policy_queues,
)
model_w, model_h = args.model_size
for cam_w, cam_h in args.camera_resolutions:
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
warp_enqueue = TinyJit(
make_warp(nv12, model_w, model_h, frame_skip, args.image_history_pipeline),
prune=True,
)
make_random_warp_inputs = make_random_blob_images(
keys=["frame", "big_frame"], size=nv12.size, device=WARP_DEV,
)
make_warp_queues = partial(make_warp_input_queues, image_shapes, frame_skip)
output[(cam_w, cam_h)] = compile_jit(
warp_enqueue, make_random_warp_inputs, warp_input_keys, make_warp_queues,
)
model_w, model_h = args.model_size
for cam_w, cam_h in args.camera_resolutions:
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
warp_enqueue = TinyJit(
make_warp(nv12, model_w, model_h, frame_skip, args.image_history_pipeline),
prune=True,
)
make_random_warp_inputs = make_random_blob_images(
keys=["frame", "big_frame"], size=nv12.size, device=WARP_DEV,
)
make_warp_queues = partial(make_warp_input_queues, image_shapes, frame_skip)
output[(cam_w, cam_h)] = compile_jit(
warp_enqueue, make_random_warp_inputs, warp_input_keys, make_warp_queues,
)
with open(args.output, "wb") as artifact_file:
if args.out_of_band:
+15 -1
View File
@@ -8,10 +8,24 @@ import numpy as np
from openpilot.system.hardware import TICI
os.environ["DEV"] = "QCOM" if TICI else "CPU"
os.environ["DEV"] = "QCOM" if TICI else os.environ.get("STARPIOT_SIM_DEV", "CPU")
from tinygrad.device import Device
from tinygrad.tensor import Tensor
if not TICI:
_sim_dev = os.environ.get("STARPIOT_SIM_DEV", "").split(":")[0].upper()
if _sim_dev:
_orig_canon = Device._canonicalize
def _remap_pc_device(device: str) -> str:
_head = device.split(":")[0].upper()
if _head in ("QCOM", "AMD", "LLVM"):
return _orig_canon(_sim_dev + device[len(_head):])
return _orig_canon(device)
Device._canonicalize = staticmethod(_remap_pc_device)
from cereal import messaging
from cereal.messaging import PubMaster, SubMaster
from msgq.visionipc import VisionBuf, VisionIpcClient, VisionStreamType
+3 -1
View File
@@ -48,7 +48,9 @@ def _fallback_tg_devices(process_name: str, usbgpu: bool) -> dict[str, str]:
# recognized. Match upstream's generated device map and select AMD directly;
# probing every tinygrad backend opens CL/DSP/CPU devices inside modeld and
# can interfere with the on-road QCOM + AMD process.
return {"WARP_DEV": backend, "QUEUE_DEV": "AMD" if usbgpu else backend}
warp_dev = os.getenv("WARP_DEV", "").strip() or backend
queue_dev = os.getenv("QUEUE_DEV", "").strip() or ("AMD" if usbgpu else backend)
return {"WARP_DEV": warp_dev, "QUEUE_DEV": queue_dev}
def get_tg_input_devices(process_name: str, usbgpu: bool) -> dict[str, str]:
+125 -8
View File
@@ -6,9 +6,28 @@ import os
import struct
from openpilot.system.hardware import TICI
os.environ['GMMU'] = '0'
os.environ['DEV'] = 'QCOM' if TICI else 'LLVM'
os.environ['DEV'] = 'QCOM' if TICI else os.environ.get('STARPIOT_SIM_DEV', 'LLVM')
from tinygrad.device import Device
from tinygrad.tensor import Tensor
# On a PC host the tinygrad JIT artifacts compiled on the comma device can bake
# QCOM/AMD device references into their graphs (a comma-device default device).
# Re-target those to the local GPU (CUDA by default) so the model loads and runs
# on the RTX instead of crashing on /dev/kgsl-3d0. Set STARPIOT_SIM_DEV=CPU to
# force the CPU backend instead. Natively-PC artifacts (CUDA/CPU) pass through
# untouched.
if not TICI:
_sim_dev = os.environ.get('STARPIOT_SIM_DEV', '').split(':')[0].upper()
if _sim_dev:
_orig_canon = Device._canonicalize
def _remap_pc_device(device: str) -> str:
_head = device.split(':')[0].upper()
if _head in ('QCOM', 'AMD', 'LLVM'):
return _orig_canon(_sim_dev + device[len(_head):])
return _orig_canon(device)
Device._canonicalize = staticmethod(_remap_pc_device)
import time
import pickle
import numpy as np
@@ -41,6 +60,7 @@ from openpilot.selfdrive.modeld.compile_modeld import (
IMAGE_HISTORY_IN_WARP,
LEGACY_WARP_INPUTS,
_detect_vision_keys,
build_supercombo_artifact,
make_split_input_queues,
make_supercombo_input_queues,
)
@@ -225,6 +245,35 @@ def _select_builtin_model(params: Params) -> None:
params.put("DrivingModelName", "Regret Driven Framework V4")
def _normalize_jit_device(jit, target_device: str) -> None:
"""Rewrite a loaded JIT's expected input devices off the comma-device alias.
The tinygrad JIT unpickler can report an input buffer's device as QCOM when a
PC artifact is loaded in this process, even though the model was compiled for a
native PC backend. The QCOM/AMD/LLVM remap keeps allocation working, but the
stored expected_input_info still carries the raw QCOM string and a later call
would fail its device comparison. Rewrite those entries to the real device so
the JIT executes.
"""
try:
captured = jit.captured
except AttributeError:
return
if captured is None:
return
info = list(getattr(captured, "expected_input_info", None) or [])
rewritten = False
for index, entry in enumerate(info):
if not isinstance(entry, (tuple, list)) or len(entry) < 4:
continue
device = entry[3]
if isinstance(device, str) and device.upper() in ("QCOM", "AMD", "LLVM"):
info[index] = (*entry[:3], target_device)
rewritten = True
if rewritten:
captured.expected_input_info = info
def _close_tinygrad_disk_cache_connection() -> None:
"""Drop tinygrad's process-global cache connection before loading the next model."""
import tinygrad.helpers as tinygrad_helpers
@@ -246,7 +295,7 @@ def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.
is_v9: bool, is_v14: bool, is_v15: bool, starpilot_toggles,
lat_smooth_seconds=LAT_SMOOTH_SECONDS, long_smooth_seconds=LONG_SMOOTH_SECONDS,
is_v16: bool = False) -> log.ModelDataV2.Action:
if is_v14 or is_v15 or is_v16:
if (is_v14 or is_v15 or is_v16) and "action" in model_output:
desired_curv_unscaled, desired_accel = model_output['action'][0]
if is_v15 or is_v16:
desired_curvature = float(desired_curv_unscaled) / max(1.0, v_ego) ** 2
@@ -318,6 +367,32 @@ def _load_model_artifact(path: Path):
return load_oob(artifact_file)
# Option 1: bypass the tinygrad JIT pickle round-trip entirely by tracing the ONNX
# live on the local device (CUDA/CPU) at modeld startup. Set STARPIOT_LIVE_ONNX to
# the path of the source .onnx to enable. This avoids the JIT-unpickler bug that
# rewrites kernel targets to the comma-device default (QCOM) and produces
# CUDA_ERROR_INVALID_IMAGE when a PC artifact is unpickled in this process.
LIVE_ONNX = os.getenv("STARPIOT_LIVE_ONNX")
def _parse_model_size(value: str) -> tuple[int, int]:
width, height = value.lower().split("x")
return int(width), int(height)
def _build_live_artifact(cam_w: int, cam_h: int) -> dict:
"""Trace the supercombo ONNX in-memory so the JITs run on the local GPU."""
model_size = _parse_model_size(os.getenv("STARPIOT_MODEL_SIZE", "512x256"))
cloudlog.warning(f"modeld: tracing supercombo ONNX live ({LIVE_ONNX}) on {Device.DEFAULT}")
return build_supercombo_artifact(
LIVE_ONNX,
model_size,
[(cam_w, cam_h)],
behavior_version=os.getenv("STARPIOT_BEHAVIOR_VERSION") or "v15",
image_history_pipeline=IMAGE_HISTORY_IN_POLICY,
)
class ModelState:
prev_desire: np.ndarray
@@ -370,8 +445,13 @@ class ModelState:
raise FileNotFoundError(model_path)
self.uses_external_gpu = external_gpu_active and requires_external_gpu and not loaded_builtin
artifact = (_load_model_artifact(model_path) if self.uses_external_gpu
else pickle.loads(read_file_chunked(str(model_path))))
if LIVE_ONNX and not self.uses_external_gpu:
# Option 1: trace the source ONNX live in-memory instead of JIT-unpickling a
# precompiled artifact, which this tinygrad build corrupts on a PC host.
artifact = _build_live_artifact(cam_w, cam_h)
else:
artifact = (_load_model_artifact(model_path) if self.uses_external_gpu
else pickle.loads(read_file_chunked(str(model_path))))
if artifact.get("format_version") != ARTIFACT_FORMAT_VERSION:
raise ValueError(
f"Unsupported model artifact format {artifact.get('format_version')!r}; "
@@ -386,9 +466,31 @@ class ModelState:
self.warp_input_keys = tuple(artifact.get("warp_input_keys", LEGACY_WARP_INPUTS))
self.policy_input_keys = tuple(artifact["policy_input_keys"])
self.run_policy = artifact["run_policy"]
self.warp_enqueue = artifact[(cam_w, cam_h)]
warp_key = (cam_w, cam_h)
if warp_key not in artifact:
# The prebuilt artifact may have been compiled for a different camera
# resolution (e.g. a --pkl fallback built at 1928x1208 while the sim now
# runs at a reduced resolution). Re-trace the ONNX at the current size so
# the warp keys match camerad instead of hard-crashing with a KeyError.
if not LIVE_ONNX:
raise KeyError(
f"model artifact has no {warp_key} warp; rebuild it for this camera resolution or trace the ONNX live"
)
cloudlog.warning(f"modeld: artifact lacks {warp_key} warp; re-tracing ONNX at current resolution")
artifact = _build_live_artifact(cam_w, cam_h)
self.warp_enqueue = artifact[warp_key]
self.can_prepare_only = self.image_history_pipeline == IMAGE_HISTORY_IN_WARP
# The tinygrad JIT unpickler can rewrite an input buffer's device to the
# comma-device default (QCOM) when loading a PC artifact in this process.
# The remap above lets allocation fall through to the target device, but the
# stored expected_input_info still compares against the raw (QCOM) string, so
# a later call would raise "args mismatch". Normalize those back to the real
# runtime device so the JITs actually run.
_normalize_jit_device(self.run_policy, str(Device.DEFAULT))
for _jit in (self.warp_enqueue,):
_normalize_jit_device(_jit, self.WARP_DEV)
if self.model_type == "supercombo":
input_shapes = self.metadata["model"]["input_shapes"]
self.output_slices = self.metadata["model"]["output_slices"]
@@ -539,13 +641,28 @@ class ModelState:
inputs: dict[str, np.ndarray], prepare_only: bool,
after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray] | None:
frames: dict[str, Tensor] = {}
_host_warp = self.WARP_DEV in ("CPU", "NPY")
for key, buf in bufs.items():
ptr = np.frombuffer(buf.data, dtype=np.uint8).ctypes.data
cache_key = (key, ptr)
if cache_key not in self._blob_cache:
self._blob_cache[cache_key] = Tensor.from_blob(
ptr, (self.frame_buf_size,), dtype="uint8", device=self.WARP_DEV,
)
if _host_warp:
# Host-mapped zero-copy view: a CPU warp can read the shared-memory
# camera buffer directly.
self._blob_cache[cache_key] = Tensor.from_blob(
ptr, (self.frame_buf_size,), dtype="uint8", device=self.WARP_DEV,
)
else:
# The camera buffer is host shared memory; a GPU warp backend cannot
# read a host pointer directly, so copy it onto the device.
self._blob_cache[cache_key] = Tensor(
np.frombuffer(buf.data, dtype=np.uint8).copy(), device=self.WARP_DEV,
).realize()
elif not _host_warp:
# Refresh the device copy with the latest frame contents.
self._blob_cache[cache_key].assign(
Tensor(np.frombuffer(buf.data, dtype=np.uint8).copy(), device=self.WARP_DEV),
).realize()
frames[key] = self._blob_cache[cache_key]
inputs[self.desire_key][0] = 0
+2 -2
View File
@@ -670,7 +670,7 @@ class SelfdriveD:
report_comm_issue, self.valid_only_comm_issue_frames = evaluate_comm_issue(
all_checks, all_alive, all_freq_ok, self.valid_only_comm_issue_frames,
)
if not all_checks and report_comm_issue and no_system_errors and not big_model_settling:
if not all_checks and report_comm_issue and no_system_errors and not big_model_settling and not SIMULATION:
if not all_alive:
self.events.add(EventName.commIssue)
elif not all_freq_ok:
@@ -689,7 +689,7 @@ class SelfdriveD:
else:
self.logged_comm_issue = None
if not self.CP.notCar and not big_model_settling:
if not self.CP.notCar and not big_model_settling and not SIMULATION:
if not self.sm['livePose'].posenetOK:
self.events.add(EventName.posenetInvalid)
if not self.sm['livePose'].inputsOK:
+16
View File
@@ -41,6 +41,16 @@ LEGACY_DRIVING_PREFIXES = (
"driving_",
)
# The bundled builtin supercombo ONNX used for live-tracing (Option 1) in modeld.
# It matches is_driving_artifact_file (legacy "driving_" prefix) but has no model
# key of its own, so model cleanup would treat it as stale and delete it out from
# under modeld. It is never downloaded, managed, or pruned.
BUILTIN_SUPERCOMBO_ONNX = "driving_supercombo.onnx"
def is_builtin_supercombo_file(filename: str) -> bool:
return filename == BUILTIN_SUPERCOMBO_ONNX
CANCEL_DOWNLOAD_PARAM = "CancelModelDownload"
DOWNLOAD_PROGRESS_PARAM = "ModelDownloadProgress"
MODEL_DOWNLOAD_PARAM = "ModelToDownload"
@@ -484,6 +494,8 @@ class ModelManager:
for model_file in MODELS_PATH.iterdir():
if not model_file.is_file() or not is_driving_artifact_file(model_file.name):
continue
if is_builtin_supercombo_file(model_file.name):
continue
model_key = model_file.name.split("_driving_", 1)[0] if "_driving_" in model_file.name else ""
if not model_key or not is_local_model_key(model_key) and model_key not in valid_keys:
delete_file(model_file, print_error=False)
@@ -623,6 +635,8 @@ class ModelManager:
for model_file in MODELS_PATH.iterdir():
if not model_file.is_file() or not is_driving_artifact_file(model_file.name):
continue
if is_builtin_supercombo_file(model_file.name):
continue
model_key = model_file.name.split("_driving_", 1)[0] if "_driving_" in model_file.name else ""
if model_key and is_local_model_key(model_key):
continue
@@ -849,6 +863,8 @@ class ModelManager:
for model_file in MODELS_PATH.iterdir():
if not model_file.is_file() or not is_driving_artifact_file(model_file.name):
continue
if is_builtin_supercombo_file(model_file.name):
continue
model_key = model_file.name.split("_driving_", 1)[0] if "_driving_" in model_file.name else ""
if model_key and is_local_model_key(model_key):
continue
+1 -1
View File
@@ -1313,7 +1313,7 @@ class StarPilotVariables:
quality_of_life_cruise = self.get_value("QOLLongitudinal") and (toggle.openpilot_longitudinal or not FPCP.pcmCruiseSpeed)
toggle.cruise_increase = self.get_value("CustomCruise", cast=float, condition=quality_of_life_cruise, default=1.0)
toggle.cruise_increase_long = self.get_value("CustomCruiseLong", cast=float, condition=quality_of_life_cruise, default=5.0)
toggle.force_stops = self.get_value("ForceStops", condition=quality_of_life_longitudinal)
toggle.force_stops = self.get_value("ForceStops", condition=quality_of_life_longitudinal) and "SIMULATION" not in os.environ
toggle.force_stop_distance_offset = self.get_value("ForceStopDistanceOffset", cast=int, condition=(quality_of_life_longitudinal and toggle.force_stops))
toggle.force_standstill = self.get_value("ForceStandstill", condition=quality_of_life_longitudinal)
toggle.radar_takeoffs = self.get_value("RadarTakeoffs", condition=quality_of_life_longitudinal)
@@ -27,7 +27,7 @@ class HybridExperimentalMode:
self.prev_a_target = 0.0
self.last_exp_dominant = False
self.diag = {}
self.record_diag = True
self.record_diag = False
# Tunings
self.HYBRID_EXP_BIAS = 0.0
@@ -107,8 +107,8 @@ class HybridExperimentalMode:
a_brake_fused = min(a_chill, a_exp)
a_throttle_fused = a_chill + max(0.0, a_exp - a_chill) * self.HYBRID_EXP_BIAS
stop_requested = bool(should_stop_chill or should_stop_exp)
is_stopping_event = stop_requested or (self.w_vision > 0.2) or horizon_stopping
# Output Arbitration
is_stopping_event = (self.w_vision > 0.3) or horizon_stopping
self.last_exp_dominant = bool(is_stopping_event and not is_departing)
if self.last_exp_dominant:
+2 -1
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import json
import math
import os
import time
import cereal.messaging as messaging
@@ -210,7 +211,7 @@ class StarPilotPlanner:
else:
self.lead_path_y = 0.0
self.raw_model_stopped = self.model_length < CRUISING_SPEED * PLANNER_TIME
self.raw_model_stopped = self.model_length < CRUISING_SPEED * PLANNER_TIME and "SIMULATION" not in os.environ
self.model_stopped = self.raw_model_stopped or self.starpilot_vcruise.forcing_stop
self.road_curvature, self.time_to_curve = calculate_road_curvature(sm["modelV2"], v_ego)
@@ -231,15 +231,6 @@ def test_should_stop_chill_handshake():
assert should_stop, "should_stop_chill must propagate into the fused stop flag"
def test_should_stop_chill_engages_braking_regime_without_vision():
controller = make_controller(prev=0.0)
model = FakeModel(velocity=[20.0] * 33)
a, should_stop = controller.update(20.0, 25.0, FakeLead(), model, 0.0, -1.5, should_stop_chill=True)
assert controller.last_exp_dominant, "should_stop_chill must engage the braking regime"
assert a == pytest.approx(-1.5, abs=1e-3), f"Must command exp braking on planner stop, got {a}"
assert should_stop
def test_should_stop_exp_handshake():
controller = make_controller(prev=0.0)
model = FakeModel(velocity=[5.0] * 33)
+2 -1
View File
@@ -11,10 +11,11 @@ from cereal import messaging
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.starpilot.common.starpilot_variables import MAPS_PATH
MAPD_DIR = Path(BASEDIR) / "starpilot/navigation"
MAPD_BIN = MAPD_DIR / "mapd"
OFFLINE_ROOT = Path("/data/media/0/osm/offline")
OFFLINE_ROOT = MAPS_PATH
RESTART_DELAY_S = 0.25
MISSING_TILE_BACKOFF_S = 30.0
FAILURE_WINDOW_S = 3.0
+1 -1
View File
@@ -248,7 +248,7 @@ SCHOOL_ZONE_SINGLE_READ_CONFIDENCE = 0.975
SCHOOL_ZONE_SHORT_CIRCUIT_CONFIDENCE = 0.78
SCHOOL_ZONE_FALLBACK_MIN_CONFIDENCE = 0.35
NON_SCHOOL_LOW_SPEED_COMPETING_MIN_CONFIDENCE = 0.95
DEBUG_BASE_DIR = Path("/data/media/0/vision_speed_limit_debug")
DEBUG_BASE_DIR = Path.home() / ".comma" / "data" / "media" / "0" / "vision_speed_limit_debug" if PC else Path("/data/media/0/vision_speed_limit_debug")
DEBUG_RUNTIME_STATUS_PATH = DEBUG_BASE_DIR / "runtime_status.json"
DEBUG_CAPTURE_DIRNAME = "captures"
SNAPSHOT_JPEG_QUALITY = 85
+4 -1
View File
@@ -8800,7 +8800,10 @@ def main():
debug = False if on_device else os.getenv("SP_GALAXY_DEBUG", "1").lower() in {"1", "true", "yes", "on"}
port = 8082 if on_device else int(os.getenv("SP_GALAXY_PORT", "8083"))
host = "0.0.0.0" if on_device else os.getenv("SP_GALAXY_HOST", "0.0.0.0")
use_reloader = False if on_device else os.getenv("SP_GALAXY_RELOAD", "0" if not debug else "1").lower() in {"1", "true", "yes", "on"}
# The Werkzeug reloader forks the process on file changes, which double-registers
# msgq publishers and crashes the stack on a host. Default it OFF (still opt-in
# via SP_GALAXY_RELOAD=1); on-device never uses the reloader.
use_reloader = False if on_device else os.getenv("SP_GALAXY_RELOAD", "0").lower() in {"1", "true", "yes", "on"}
if debug:
print("\"The Galaxy\" is not running on a comma device, enabling debug mode")
+18 -12
View File
@@ -57,18 +57,24 @@ print(_manager_import_timing_line, flush=True)
_append_boot_timing_line(_manager_import_timing_line)
LEGACY_BOLT_FP_MIGRATION_FLAG = Path("/data") / "legacy_bolt_fp_migration_v1"
STARPILOT_DEFAULTS_PARITY_MIGRATION_FLAG = Path("/data") / "starpilot_defaults_parity_v1"
STARPILOT_HUMANLIKE_DISABLE_MIGRATION_FLAG = Path("/data") / "starpilot_humanlike_disable_v1"
STARPILOT_CLUSTER_OFFSET_MIGRATION_FLAG = Path("/data") / "starpilot_cluster_offset_v1"
STARPILOT_TRAFFIC_SMOOTH_MIGRATION_FLAG = Path("/data") / "starpilot_traffic_smooth_v1"
STARPILOT_TRAFFIC_FOLLOW_MIGRATION_FLAG = Path("/data") / "starpilot_traffic_follow_v1"
STARPILOT_PARAM_RENAME_MIGRATION_FLAG = Path("/data") / "starpilot_param_rename_v1"
STARPILOT_PARAM_CANONICALIZATION_MIGRATION_FLAG = Path("/data") / "starpilot_param_canonicalization_v1"
STARPILOT_PC_ROOT_MIGRATION_FLAG = Path("/data") / "starpilot_pc_root_v1"
STARPILOT_PARAMS_CACHE_MIGRATION_FLAG = Path("/data") / "starpilot_params_cache_v1"
STARPILOT_DEFAULT_MODEL_MIGRATION_FLAG = Path("/data") / "starpilot_default_model_rdf_v4"
STARPILOT_CE_MODEL_STOP_TIME_MIGRATION_FLAG = Path("/data") / "starpilot_ce_model_stop_time_v2"
# Migration-flag root. On device these live on /data; on a PC host (sim, WSL,
# no sudo) /data does not exist and is not writable, which made every migration
# flag write fail with a PermissionError on each startup. Fall back to the
# user-writable comma home on PC so the flags persist across runs.
MIGRATION_FLAG_ROOT = Path("/data") if HARDWARE.get_device_type() != "pc" else Path(Paths.comma_home()) / "migrations"
LEGACY_BOLT_FP_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "legacy_bolt_fp_migration_v1"
STARPILOT_DEFAULTS_PARITY_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_defaults_parity_v1"
STARPILOT_HUMANLIKE_DISABLE_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_humanlike_disable_v1"
STARPILOT_CLUSTER_OFFSET_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_cluster_offset_v1"
STARPILOT_TRAFFIC_SMOOTH_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_traffic_smooth_v1"
STARPILOT_TRAFFIC_FOLLOW_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_traffic_follow_v1"
STARPILOT_PARAM_RENAME_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_param_rename_v1"
STARPILOT_PARAM_CANONICALIZATION_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_param_canonicalization_v1"
STARPILOT_PC_ROOT_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_pc_root_v1"
STARPILOT_PARAMS_CACHE_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_params_cache_v1"
STARPILOT_DEFAULT_MODEL_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_default_model_rdf_v4"
STARPILOT_CE_MODEL_STOP_TIME_MIGRATION_FLAG = MIGRATION_FLAG_ROOT / "starpilot_ce_model_stop_time_v2"
STARPILOT_LEGACY_CACHE_MARKER_KEYS = ("RemapCancelToDistance",)
STARPILOT_REMOVED_PARAM_KEYS = (
"CoastUpToLeads", "HumanAcceleration", "HumanFollowing", "PrioritizeSmoothFollowing", "ReverseCruise",
@@ -22,7 +22,7 @@ class ClangCompiler(Compiler):
return subprocess.check_output([getenv("CC", 'clang'), '-c', '-x', 'c', '-O2', '-fPIC', '-ffreestanding', '-fno-math-errno', '-nostdlib',
'-fno-ident', f'--target={self.arch}-none-unknown-elf', *self.args, '-', '-o', '-'], input=src.encode('utf-8'))
def compile(self, src:str) -> bytes: return jit_loader(self.compile_to_obj(src))
def compile(self, src:str) -> bytes: return jit_loader(self.compile_to_obj(src), link_libs=["m"])
def disassemble(self, lib:bytes): return capstone_flatdump(lib, self.arch)
-893
View File
@@ -1,893 +0,0 @@
#!/usr/bin/env python3
"""HEM Live Inspector & Diagnostic Studio.
An interactive, live-scrubbing telemetry debugger for Hybrid Experimental Mode (HEM)
and Pure Experimental Mode comparisons.
"""
from __future__ import annotations
import argparse
import bisect
import http.server
import json
import os
import socketserver
import sys
import threading
import time
import urllib.parse
import webbrowser
from pathlib import Path
from types import SimpleNamespace
from typing import Any
import numpy as np
# Ensure openpilot root is in sys.path
ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner
from openpilot.starpilot.controls.lib.hybrid_experimental_mode import HybridExperimentalMode
from openpilot.tools.lib.logreader import LogReader, ReadMode, parse_direct, parse_indirect
from openpilot.tools.lib.route import SegmentRange
SERVICES = {
"carState", "radarState", "starpilotRadarState", "modelV2",
"longitudinalPlan", "selfdriveState", "starpilotPlan", "starpilotCarState",
"controlsState", "carParams", "liveParameters", "carControl",
}
def as_lead(raw):
if raw is None:
return SimpleNamespace(status=False, dRel=150.0, yRel=0.0, vRel=0.0, aRel=0.0,
vLead=0.0, vLeadK=0.0, dPath=0.0, vLat=0.0, aLeadK=0.0,
aLeadTau=1.5, fcw=False, modelProb=0.0, radar=False)
return SimpleNamespace(
status=bool(getattr(raw, "status", False)),
dRel=float(getattr(raw, "dRel", 150.0)),
yRel=float(getattr(raw, "yRel", 0.0)),
vRel=float(getattr(raw, "vRel", 0.0)),
aRel=float(getattr(raw, "aRel", 0.0)),
vLead=float(getattr(raw, "vLead", 0.0)),
vLeadK=float(getattr(raw, "vLeadK", getattr(raw, "vLead", 0.0))),
dPath=float(getattr(raw, "dPath", 0.0)),
vLat=float(getattr(raw, "vLat", 0.0)),
aLeadK=float(getattr(raw, "aLeadK", 0.0)),
aLeadTau=float(getattr(raw, "aLeadTau", 1.5)),
fcw=bool(getattr(raw, "fcw", False)),
modelProb=float(getattr(raw, "modelProb", 0.0)),
radar=bool(getattr(raw, "radar", False)),
)
def load_route_data(route_str: str, segment: int, data_dir: str | None = None):
print(f"[HEM Inspector] Resolving route: {route_str} (segment {segment})...")
direct = parse_direct(route_str)
if direct is not None:
identifiers = [str(direct)]
else:
parsed = parse_indirect(route_str)
sr = SegmentRange(parsed)
route_name = sr.route_name.replace("/", "|")
identifiers = [f"{route_name}--{segment}"]
bufs = {s: [] for s in SERVICES}
for identifier in identifiers:
try:
lr = LogReader(identifier, default_mode=ReadMode.RLOG)
except Exception as exc:
print(f"[HEM Inspector] Error reading {identifier}: {exc}", file=sys.stderr)
return None
for evt in lr:
which = evt.which()
if which in SERVICES:
t = evt.logMonoTime * 1e-9
bufs[which].append((t, getattr(evt, which)))
for s in SERVICES:
bufs[s].sort(key=lambda x: x[0])
if not any(bufs[s] for s in SERVICES):
print("[HEM Inspector] No telemetry messages found in segment.", file=sys.stderr)
return None
return bufs
def run_hem_replay(bufs, start_sec: float | None, end_sec: float | None, toggles: Any):
ts = {s: [t for t, _ in m] for s, m in bufs.items()}
ms = {s: [m for _, m in bufs[s]] for s in bufs}
def latest(service, t):
times = ts.get(service)
if not times:
return None
idx = bisect.bisect_right(times, t) - 1
return ms[service][idx] if idx >= 0 else None
cp_candidate = None
for _, cp in bufs.get("carParams", []):
if cp is not None:
cp_candidate = cp
break
if cp_candidate is None:
cp_candidate = SimpleNamespace(
brand="toyota", carFingerprint="TOYOTA_RAV4", openpilotLongitudinalControl=True,
pcmCruise=False, steerRatio=15.0, wheelbase=2.7, longitudinalActuatorDelay=0.2, flags=0,
)
req_services = [s for s in ("modelV2", "carState") if len(bufs.get(s, []))]
all_min = max(bufs[s][0][0] for s in req_services) if req_services else min(t for s in SERVICES for t, _ in bufs[s] if len(bufs[s]))
all_max = max(t for s in SERVICES for t, _ in bufs[s] if len(bufs[s]))
start = all_min if start_sec is None else all_min + start_sec
end = all_max if end_sec is None else min(all_max, all_min + end_sec)
grid = np.arange(start, end, DT_MDL)
planner = LongitudinalPlanner(cp_candidate, dt=DT_MDL)
hybrid = HybridExperimentalMode()
hybrid.record_diag = True
hybrid.set_tuning(getattr(toggles, "hybrid_exp_bias", 0.0), getattr(toggles, "hybrid_vision_brake_sensitivity", 1.0))
frames = []
stop_events = []
real_monotonic = time.monotonic
fake_clock = [0.0]
time.monotonic = lambda: fake_clock[0]
try:
for i, t in enumerate(grid):
fake_clock[0] = float(i) * DT_MDL
t_rel = round(t - all_min, 3)
car_raw = latest("carState", t)
model_v2 = latest("modelV2", t)
if model_v2 is None or car_raw is None:
continue
v_ego = max(float(getattr(car_raw, "vEgo", 0.0)), 0.0)
car = SimpleNamespace(
vEgo=v_ego,
vEgoCluster=max(float(getattr(car_raw, "vEgoCluster", v_ego)), 0.0),
vCruise=float(getattr(car_raw, "vCruise", 0.0)),
standstill=bool(getattr(car_raw, "standstill", False)),
leftBlinker=bool(getattr(car_raw, "leftBlinker", False)),
rightBlinker=bool(getattr(car_raw, "rightBlinker", False)),
gasPressed=bool(getattr(car_raw, "gasPressed", False)),
brakePressed=bool(getattr(car_raw, "brakePressed", False)),
steeringAngleDeg=float(getattr(car_raw, "steeringAngleDeg", 0.0)),
aEgo=float(getattr(car_raw, "aEgo", 0.0)),
leftBlindspot=bool(getattr(car_raw, "leftBlindspot", False)),
rightBlindspot=bool(getattr(car_raw, "rightBlindspot", False)),
)
radar = latest("radarState", t)
lead1 = as_lead(getattr(radar, "leadOne", None) if radar else None)
lead2 = as_lead(getattr(radar, "leadTwo", None) if radar else None)
lplan = latest("longitudinalPlan", t)
sds = latest("selfdriveState", t)
splan = latest("starpilotPlan", t)
scs = latest("starpilotCarState", t)
live_params = latest("liveParameters", t)
car_control = latest("carControl", t)
controls_state = latest("controlsState", t)
# Logged in-drive states
logged_exp_mode = bool(getattr(sds, "experimentalMode", False))
logged_a_target = float(getattr(lplan, "aTarget", 0.0)) if lplan else 0.0
logged_should_stop = bool(getattr(lplan, "shouldStop", False)) if lplan else False
v_cruise = float(getattr(splan, "vCruise", 0.0) or 0.0)
if not (v_cruise > 0):
v_cruise_kph = float(getattr(car_raw, "vCruise", 0.0) or 0.0)
v_cruise = min(v_cruise_kph, V_CRUISE_MAX) * CV.KPH_TO_MS if 0 < v_cruise_kph < V_CRUISE_UNSET else v_ego
tracking_lead = bool(getattr(splan, "trackingLead", False) or getattr(lplan, "hasLead", False))
t_follow = float(getattr(splan, "tFollow", 1.45))
forcing_stop = bool(getattr(splan, "forcingStop", False))
sm_dict = {
"carState": car,
"radarState": SimpleNamespace(leadOne=lead1, leadTwo=lead2),
"starpilotRadarState": SimpleNamespace(leadLeft=as_lead(None), leadRight=as_lead(None)),
"starpilotCarState": SimpleNamespace(
trafficModeEnabled=bool(getattr(scs, "trafficModeEnabled", False)),
alwaysOnLateralEnabled=bool(getattr(scs, "alwaysOnLateralEnabled", False)),
accelPressed=bool(getattr(scs, "accelPressed", False)),
),
"selfdriveState": SimpleNamespace(
enabled=bool(getattr(sds, "enabled", False)),
experimentalMode=False,
personality=int(getattr(getattr(sds, "personality", None), "raw", 0)),
),
"longitudinalPlan": SimpleNamespace(
hasLead=bool(getattr(lplan, "hasLead", False)),
allowThrottle=bool(getattr(lplan, "allowThrottle", True)),
shouldStop=bool(getattr(lplan, "shouldStop", False)),
aTarget=float(getattr(lplan, "aTarget", 0.0)),
),
"starpilotPlan": SimpleNamespace(
vCruise=v_cruise,
tFollow=t_follow,
trackingLead=tracking_lead,
redLight=bool(getattr(splan, "redLight", False)),
forcingStop=forcing_stop,
forcingStopLength=float(getattr(splan, "forcingStopLength", 100.0)),
minAcceleration=float(getattr(splan, "minAcceleration", -3.5)),
maxAcceleration=float(getattr(splan, "maxAcceleration", 1.5)),
accelerationJerk=float(getattr(splan, "accelerationJerk", 1.0)),
dangerJerk=float(getattr(splan, "dangerJerk", 1.0)),
speedJerk=float(getattr(splan, "speedJerk", 1.0)),
dangerFactor=float(getattr(splan, "dangerFactor", 1.0)),
disableThrottle=bool(getattr(splan, "disableThrottle", False)),
),
"controlsState": SimpleNamespace(
longControlState=getattr(controls_state, "longControlState", LongCtrlState.pid),
forceDecel=bool(getattr(controls_state, "forceDecel", False)),
curvature=float(getattr(controls_state, "curvature", 0.0)),
),
"liveParameters": SimpleNamespace(
angleOffsetDeg=float(getattr(live_params, "angleOffsetDeg", 0.0)) if live_params else 0.0,
),
"carControl": SimpleNamespace(
orientationNED=getattr(car_control, "orientationNED", [0.0, 0.0, 0.0]) if car_control else [0.0, 0.0, 0.0],
),
"modelV2": model_v2,
"carParams": cp_candidate,
}
# 1. Update Chill Planner (ACC MPC Baseline)
planner.update(sm_dict, toggles)
a_chill = float(planner.output_a_target)
should_stop_chill = bool(planner.output_should_stop)
# 2. Extract Vision NN Action Intent & Horizon Metrics
a_exp = 0.0
should_stop_exp = False
stop_line_prob = 0.0
stop_line_dist = 0.0
if model_v2 is not None:
act = getattr(model_v2, "action", None)
if act is not None:
a_exp = float(getattr(act, "desiredAcceleration", 0.0))
should_stop_exp = bool(getattr(act, "shouldStop", False))
stop_line = getattr(model_v2, "stopLine", None)
if stop_line is not None:
stop_line_prob = float(getattr(stop_line, "prob", 0.0))
stop_line_dist = float(getattr(stop_line, "distance", 0.0))
# 3. Step HEM Fusion Machine
active_lead = lead2 if planner.mpc.source == "lead1" else lead1
a_fused, should_stop_fused = hybrid.update(
v_ego=v_ego, v_cruise=v_cruise, lead_one=active_lead,
model_v2=model_v2, a_chill=a_chill, a_exp=a_exp,
should_stop_exp=should_stop_exp, should_stop_chill=should_stop_chill,
gas_pressed=car.gasPressed,
)
diag_dump = dict(hybrid.diag) if hasattr(hybrid, "diag") else {}
exp_dominant = bool(getattr(hybrid, "last_exp_dominant", False) or hybrid.w_vision > 0.65)
if should_stop_exp or (exp_dominant and a_exp < -1.0) or (logged_should_stop and logged_exp_mode):
if not stop_events or (t_rel - stop_events[-1]["time"]) > 3.0:
stop_events.append({"time": t_rel, "reason": "Stop Detected" if (should_stop_exp or logged_should_stop) else "Vision Decel"})
frame_payload = {
"idx": i,
"t": t_rel,
"v_ego": round(v_ego, 2),
"v_ego_mph": round(v_ego * CV.MS_TO_MPH, 1),
"v_cruise_mph": round(v_cruise * CV.MS_TO_MPH, 1),
"a_ego": round(car.aEgo, 2),
"gas_pressed": car.gasPressed,
"brake_pressed": car.brakePressed,
"standstill": car.standstill,
"logged_drive": {
"experimental_mode": logged_exp_mode,
"a_target": round(logged_a_target, 2),
"should_stop": logged_should_stop,
},
"lead": {
"status": active_lead.status,
"dRel": round(active_lead.dRel, 1),
"vLead": round(active_lead.vLead, 1),
"aLeadK": round(active_lead.aLeadK, 2),
"radar": active_lead.radar,
},
"model": {
"should_stop_exp": should_stop_exp,
"a_exp": round(a_exp, 2),
"stop_line_prob": round(stop_line_prob, 3),
"stop_line_dist": round(stop_line_dist, 1),
},
"planner": {
"a_chill": round(a_chill, 2),
"should_stop_chill": should_stop_chill,
"mpc_source": planner.mpc.source,
"v_desired_now": round(float(planner.v_desired_trajectory[0]), 2) if len(planner.v_desired_trajectory) else 0.0,
},
"hem": {
"a_fused": round(a_fused, 2),
"should_stop_fused": should_stop_fused,
"w_vision": round(float(hybrid.w_vision), 3),
"exp_dominant": exp_dominant,
"diag": diag_dump,
},
}
frames.append(frame_payload)
finally:
time.monotonic = real_monotonic
return frames, stop_events
INDEX_HTML = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HEM & Pure EXP Live Inspector</title>
<style>
:root {
--bg: #0f1117; --panel: #181b23; --border: #282c37; --text: #e1e4ea;
--accent: #3b82f6; --exp-orange: #ff9800; --chill-blue: #0ea5e9; --danger: #ef4444; --green: #22c55e; --logged-purple: #c084fc;
}
* { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
body { background: var(--bg); color: var(--text); display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
header { background: var(--panel); border-bottom: 1px solid var(--border); padding: 8px 16px; display: flex; justify-content: space-between; align-items: center; }
.title { font-size: 15px; font-weight: 700; display: flex; align-items: center; gap: 8px; }
.badge { background: #334155; padding: 2px 7px; border-radius: 4px; font-size: 11px; text-transform: uppercase; }
.badge.orange { background: #7c2d12; color: #ffedd5; border: 1px solid var(--exp-orange); }
.badge.purple { background: #581c87; color: #f3e8ff; border: 1px solid var(--logged-purple); }
.export-bar { display: flex; align-items: center; gap: 6px; background: #1e293b; border: 1px solid #334155; border-radius: 6px; padding: 4px 10px; }
.export-bar label { font-size: 11px; color: #94a3b8; font-weight: 600; }
.export-bar input { width: 55px; background: #0f1117; color: #38bdf8; border: 1px solid #475569; border-radius: 4px; padding: 3px 6px; font-size: 12px; font-weight: bold; text-align: center; }
.main-layout { display: grid; grid-template-columns: 1fr 380px; flex: 1; overflow: hidden; }
.left-pane { display: flex; flex-direction: column; padding: 12px; gap: 12px; overflow-y: auto; }
.right-pane { background: var(--panel); border-left: 1px solid var(--border); padding: 12px; display: flex; flex-direction: column; gap: 12px; overflow-y: auto; }
.card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; position: relative; }
.card.hem-border-active { border: 2px solid var(--exp-orange); box-shadow: 0 0 15px rgba(255, 152, 0, 0.25); }
.card-title { font-size: 11px; font-weight: 600; text-transform: uppercase; color: #94a3b8; margin-bottom: 8px; display: flex; justify-content: space-between; }
.controls-bar { display: flex; align-items: center; gap: 8px; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 8px 12px; }
button { background: #334155; color: white; border: none; border-radius: 5px; padding: 5px 10px; font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.15s; }
button:hover { background: #475569; }
button.primary { background: var(--accent); }
button.primary:hover { background: #2563eb; }
button.copy-btn { background: #059669; }
button.copy-btn:hover { background: #047857; }
.slider-container { flex: 1; display: flex; flex-direction: column; gap: 3px; }
input[type=range] { width: 100%; accent-color: var(--accent); cursor: pointer; }
.canvas-container { position: relative; height: 165px; width: 100%; }
canvas { width: 100%; height: 100%; display: block; }
.metric-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.metric-box { background: #0f1117; border: 1px solid var(--border); border-radius: 6px; padding: 6px; text-align: center; }
.metric-val { font-size: 15px; font-weight: 700; margin-top: 2px; }
.metric-lbl { font-size: 10px; color: #64748b; text-transform: uppercase; }
.meter { height: 8px; background: #334155; border-radius: 4px; overflow: hidden; margin-top: 5px; }
.meter-fill { height: 100%; width: 0%; background: linear-gradient(90deg, var(--chill-blue), var(--exp-orange)); transition: width 0.05s; }
table.diag-table { width: 100%; border-collapse: collapse; font-size: 11px; font-family: monospace; }
table.diag-table td { padding: 3px 5px; border-bottom: 1px solid #232733; }
table.diag-table td.key { color: #94a3b8; }
table.diag-table td.val { text-align: right; color: #38bdf8; font-weight: 600; }
.events-list { display: flex; flex-wrap: wrap; gap: 6px; max-height: 70px; overflow-y: auto; }
.event-chip { background: #1e293b; border: 1px solid #334155; border-radius: 4px; padding: 3px 8px; font-size: 11px; cursor: pointer; }
.event-chip:hover { border-color: var(--exp-orange); color: var(--exp-orange); }
</style>
</head>
<body>
<header>
<div class="title">
<span>🚘 HEM & Pure EXP Inspector</span>
<span id="driveModeBadge" class="badge">DRIVE: CHILL</span>
<span id="expBadge" class="badge">CHILL DOMINANT</span>
<span id="stopSignBadge" class="badge orange" style="display:none;">🛑 STOP DETECTED</span>
</div>
<div style="display: flex; gap: 8px; align-items: center;">
<div class="export-bar">
<label>Window:</label>
<input type="number" id="exportStart" value="12" step="0.5">
<span style="color:#64748b;">to</span>
<input type="number" id="exportEnd" value="19" step="0.5">
<label>sec</label>
<button onclick="setExportWindowToCurrent()" style="padding:3px 6px; font-size:10px;">📍 Here ±3s</button>
</div>
<button class="copy-btn" onclick="copyDiagnosticRange()">📋 Copy Range Log (for AI)</button>
<button onclick="copySingleSnapshot()" style="background:#334155;">📷 1-Frame</button>
</div>
</header>
<div class="main-layout">
<div class="left-pane">
<div class="controls-bar">
<button onclick="togglePlay()" id="playBtn">▶ Play</button>
<button onclick="step(-1)">❮ 50ms</button>
<button onclick="step(1)">50ms ❯</button>
<button onclick="step(-20)">❮ 1s</button>
<button onclick="step(20)">1s ❯</button>
<div class="slider-container">
<input type="range" id="timeSlider" min="0" max="100" step="1" value="0" oninput="onSlider(this.value)">
<div style="display:flex; justify-content:space-between; font-size:11px; color:#64748b;">
<span id="curTimeLbl">0.00s</span>
<span id="totalTimeLbl">0.00s</span>
</div>
</div>
<select id="speedSel" onchange="playbackSpeed = parseFloat(this.value)" style="background:#334155; color:white; border:none; padding:4px 6px; border-radius:4px; font-size:11px;">
<option value="0.1">0.1x (Slow)</option>
<option value="0.25">0.25x</option>
<option value="0.5">0.5x</option>
<option value="1.0" selected>1.0x</option>
<option value="2.0">2.0x</option>
</select>
</div>
<div class="card" style="padding:8px 12px;">
<div class="card-title">Jump to Events / Stop Candidates</div>
<div class="events-list" id="eventsList"></div>
</div>
<div class="card" id="mainCard">
<div class="card-title">
<span>Acceleration Trajectory Comparison (m/s²)</span>
<span style="display:flex; gap:10px; font-size:10px;">
<span style="color:#c084fc; font-weight:bold;">━ a_logged (Car)</span>
<span style="color:#ff9800;">■ a_exp (Model)</span>
<span style="color:#0ea5e9;">■ a_chill (MPC)</span>
<span style="color:#38bdf8; font-weight:bold;">━ a_fused (HEM)</span>
</span>
</div>
<div class="canvas-container">
<canvas id="accelCanvas"></canvas>
</div>
</div>
<div class="metric-grid">
<div class="metric-box">
<div class="metric-lbl">Speed (Ego / Set)</div>
<div class="metric-val" id="mSpeed">0 / 0 <span style="font-size:10px; font-weight:normal;">mph</span></div>
</div>
<div class="metric-box">
<div class="metric-lbl">Logged Drive Accel</div>
<div class="metric-val" id="mLogged" style="color:#c084fc;">0.00 <span style="font-size:10px; font-weight:normal;">m/s²</span></div>
</div>
<div class="metric-box">
<div class="metric-lbl">Simulated HEM Accel</div>
<div class="metric-val" id="mFused" style="color:#38bdf8;">0.00 <span style="font-size:10px; font-weight:normal;">m/s²</span></div>
</div>
<div class="metric-box">
<div class="metric-lbl">Lead Distance</div>
<div class="metric-val" id="mLead">None</div>
</div>
</div>
</div>
<div class="right-pane">
<div class="card">
<div class="card-title">HEM Authority (Vision Weight)</div>
<div style="display:flex; justify-content:space-between; font-size:16px; font-weight:bold;">
<span id="authVal">0.00</span>
<span id="authDesc" style="font-size:11px; color:#94a3b8;">Chill Cruise Mode</span>
</div>
<div class="meter"><div class="meter-fill" id="authMeter"></div></div>
</div>
<div class="card">
<div class="card-title">Stop Intent Matrix</div>
<table class="diag-table">
<tr><td class="key">Logged In-Car Mode</td><td class="val" id="dLoggedMode">CHILL</td></tr>
<tr><td class="key">Logged In-Car Stop</td><td class="val" id="dLoggedStop">False</td></tr>
<tr><td class="key">model.shouldStop (Exp)</td><td class="val" id="dModelStop">False</td></tr>
<tr><td class="key">planner.shouldStop (Chill)</td><td class="val" id="dChillStop">False</td></tr>
<tr><td class="key">HEM Fused shouldStop</td><td class="val" id="dFusedStop">False</td></tr>
<tr><td class="key">Stop Line Prob / Dist</td><td class="val" id="dStopLine">0.00 / 0.0m</td></tr>
</table>
</div>
<div class="card" style="flex:1;">
<div class="card-title">HEM Internal Diag State Dump</div>
<table class="diag-table" id="diagTableBody"></table>
</div>
</div>
</div>
<script>
let frames = [];
let curIdx = 0;
let isPlaying = false;
let playbackSpeed = 1.0;
let playTimer = null;
fetch('/api/data').then(r => r.json()).then(data => {
frames = data.frames;
document.getElementById('timeSlider').max = frames.length - 1;
const maxT = frames[frames.length - 1].t;
document.getElementById('totalTimeLbl').innerText = maxT.toFixed(2) + 's';
const el = document.getElementById('eventsList');
if (data.events.length === 0) el.innerHTML = '<span style="font-size:11px; color:#64748b;">No high-decel stop events found.</span>';
data.events.forEach(ev => {
const chip = document.createElement('div');
chip.className = 'event-chip';
chip.innerText = `⏱ ${ev.time.toFixed(1)}s - ${ev.reason}`;
chip.onclick = () => {
seekTime(ev.time);
document.getElementById('exportStart').value = Math.max(0, ev.time - 3).toFixed(1);
document.getElementById('exportEnd').value = Math.min(maxT, ev.time + 3).toFixed(1);
};
el.appendChild(chip);
});
render();
});
function setExportWindowToCurrent() {
if (!frames.length) return;
const t = frames[curIdx].t;
const maxT = frames[frames.length - 1].t;
document.getElementById('exportStart').value = Math.max(0, t - 3.0).toFixed(1);
document.getElementById('exportEnd').value = Math.min(maxT, t + 3.0).toFixed(1);
}
function onSlider(v) {
curIdx = parseInt(v);
render();
}
function step(delta) {
curIdx = Math.max(0, Math.min(frames.length - 1, curIdx + delta));
document.getElementById('timeSlider').value = curIdx;
render();
}
function seekTime(targetT) {
let best = 0;
for (let i = 0; i < frames.length; i++) {
if (frames[i].t >= targetT) { best = i; break; }
}
curIdx = best;
document.getElementById('timeSlider').value = curIdx;
render();
}
function togglePlay() {
isPlaying = !isPlaying;
document.getElementById('playBtn').innerText = isPlaying ? '⏸ Pause' : '▶ Play';
if (isPlaying) scheduleNextFrame();
else clearTimeout(playTimer);
}
function scheduleNextFrame() {
if (!isPlaying) return;
step(1);
if (curIdx >= frames.length - 1) { togglePlay(); return; }
playTimer = setTimeout(scheduleNextFrame, (50 / playbackSpeed));
}
function render() {
if (!frames.length) return;
const f = frames[curIdx];
document.getElementById('curTimeLbl').innerText = f.t.toFixed(2) + 's';
// Drive Mode
const dmb = document.getElementById('driveModeBadge');
if (f.logged_drive.experimental_mode) {
dmb.innerText = 'LOGGED DRIVE: PURE EXP';
dmb.className = 'badge purple';
} else {
dmb.innerText = 'LOGGED DRIVE: CHILL/HEM';
dmb.className = 'badge';
}
// Badges & border
const expBadge = document.getElementById('expBadge');
const mainCard = document.getElementById('mainCard');
if (f.hem.exp_dominant) {
expBadge.innerText = 'HEM EXP DOMINANT';
expBadge.className = 'badge orange';
mainCard.className = 'card hem-border-active';
} else {
expBadge.innerText = 'HEM CHILL DOMINANT';
expBadge.className = 'badge';
mainCard.className = 'card';
}
document.getElementById('stopSignBadge').style.display = (f.model.should_stop_exp || f.logged_drive.should_stop) ? 'inline-block' : 'none';
// Metrics
document.getElementById('mSpeed').innerText = `${f.v_ego_mph} / ${f.v_cruise_mph}`;
document.getElementById('mLogged').innerText = f.logged_drive.a_target.toFixed(2);
document.getElementById('mFused').innerText = f.hem.a_fused.toFixed(2);
document.getElementById('mLead').innerText = f.lead.status ? `${f.lead.dRel}m (${f.lead.vLead} m/s)` : 'None';
// Authority
document.getElementById('authVal').innerText = f.hem.w_vision.toFixed(2);
document.getElementById('authMeter').style.width = (f.hem.w_vision * 100) + '%';
document.getElementById('authDesc').innerText = f.hem.w_vision > 0.65 ? 'Vision Dominant' : (f.hem.w_vision < 0.35 ? 'ACC Chill Dominant' : 'Blended Authority');
// Intent
document.getElementById('dLoggedMode').innerText = f.logged_drive.experimental_mode ? 'PURE EXPERIMENTAL' : 'CHILL / MPC';
document.getElementById('dLoggedMode').style.color = f.logged_drive.experimental_mode ? '#c084fc' : '#38bdf8';
document.getElementById('dLoggedStop').innerText = f.logged_drive.should_stop ? '🛑 TRUE' : 'False';
document.getElementById('dLoggedStop').style.color = f.logged_drive.should_stop ? '#ef4444' : '#94a3b8';
document.getElementById('dModelStop').innerText = f.model.should_stop_exp ? '🛑 TRUE' : 'False';
document.getElementById('dModelStop').style.color = f.model.should_stop_exp ? '#ef4444' : '#94a3b8';
document.getElementById('dChillStop').innerText = f.planner.should_stop_chill ? 'TRUE' : 'False';
document.getElementById('dFusedStop').innerText = f.hem.should_stop_fused ? '🛑 TRUE' : 'False';
document.getElementById('dFusedStop').style.color = f.hem.should_stop_fused ? '#ef4444' : '#94a3b8';
document.getElementById('dStopLine').innerText = `${f.model.stop_line_prob} / ${f.model.stop_line_dist}m`;
// Diag Table
const tbody = document.getElementById('diagTableBody');
tbody.innerHTML = '';
for (const [k, v] of Object.entries(f.hem.diag)) {
const tr = document.createElement('tr');
tr.innerHTML = `<td class="key">${k}</td><td class="val">${typeof v === 'number' ? v.toFixed(3) : v}</td>`;
tbody.appendChild(tr);
}
drawGraph();
}
function drawGraph() {
const canvas = document.getElementById('accelCanvas');
const ctx = canvas.getContext('2d');
canvas.width = canvas.parentElement.clientWidth;
canvas.height = canvas.parentElement.clientHeight;
const w = canvas.width, h = canvas.height;
ctx.clearRect(0, 0, w, h);
const zeroY = h * 0.45;
ctx.strokeStyle = '#334155';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, zeroY); ctx.lineTo(w, zeroY); ctx.stroke();
const span = 60;
const start = Math.max(0, curIdx - span/2);
const end = Math.min(frames.length - 1, start + span);
const scaleY = h / 7.0;
function plotLine(keyFn, color, width, dash = []) {
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.setLineDash(dash);
ctx.beginPath();
for (let i = start; i <= end; i++) {
const x = ((i - start) / span) * w;
const val = keyFn(frames[i]);
const y = zeroY - val * scaleY;
if (i === start) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.stroke();
ctx.setLineDash([]);
}
plotLine(f => f.logged_drive.a_target, '#c084fc', 2.0, [3, 2]); // Actual logged drive
plotLine(f => f.planner.a_chill, '#0ea5e9', 1.2, [4, 4]); // Chill MPC
plotLine(f => f.model.a_exp, '#ff9800', 1.2, [2, 2]); // Vision Model
plotLine(f => f.hem.a_fused, '#38bdf8', 2.2); // Simulated HEM
// Cursor
const curX = ((curIdx - start) / span) * w;
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(curX, 0); ctx.lineTo(curX, h); ctx.stroke();
}
function copySingleSnapshot() {
const f = frames[curIdx];
const payload = {
diagnostic_type: "HEM_SINGLE_FRAME_SNAPSHOT",
timestamp_sec: f.t,
logged_actual_drive: f.logged_drive,
vehicle_state: {
v_ego_mph: f.v_ego_mph,
a_ego: f.a_ego,
gas_pressed: f.gas_pressed,
brake_pressed: f.brake_pressed,
standstill: f.standstill,
},
lead: f.lead,
model_vision: f.model,
planner_acc_mpc: f.planner,
hem_fusion_engine: f.hem,
};
const md = "```json\\n" + JSON.stringify(payload, null, 2) + "\\n```";
navigator.clipboard.writeText(md).then(() => {
alert("Copied 1-frame snapshot to clipboard!");
});
}
function copyDiagnosticRange() {
const startT = parseFloat(document.getElementById('exportStart').value) || 0.0;
const endT = parseFloat(document.getElementById('exportEnd').value) || 0.0;
if (startT >= endT) {
alert("Start time must be strictly less than End time.");
return;
}
const subset = frames.filter(f => f.t >= startT && f.t <= endT);
if (!subset.length) {
alert("No telemetry frames found in specified time range.");
return;
}
let shouldStopFlicks = 0;
let expDominantFlicks = 0;
let leadStatusFlicks = 0;
let minALogged = 999.0;
let minAExp = 999.0;
let minAChill = 999.0;
let minAFused = 999.0;
let maxWVision = 0.0;
let minWVision = 1.0;
let isPureExpRun = subset.some(f => f.logged_drive.experimental_mode);
for (let i = 0; i < subset.length; i++) {
const cur = subset[i];
minALogged = Math.min(minALogged, cur.logged_drive.a_target);
minAExp = Math.min(minAExp, cur.model.a_exp);
minAChill = Math.min(minAChill, cur.planner.a_chill);
minAFused = Math.min(minAFused, cur.hem.a_fused);
maxWVision = Math.max(maxWVision, cur.hem.w_vision);
minWVision = Math.min(minWVision, cur.hem.w_vision);
if (i > 0) {
const prev = subset[i - 1];
if (cur.model.should_stop_exp !== prev.model.should_stop_exp) shouldStopFlicks++;
if (cur.hem.exp_dominant !== prev.hem.exp_dominant) expDominantFlicks++;
if (cur.lead.status !== prev.lead.status) leadStatusFlicks++;
}
}
let tableMd = "| Time(s) | vEgo(mph) | a_logged | a_exp | a_chill | a_fused | w_vis | ExpStop | LoggedStop | FusedStop | Lead(dRel,vRel) | Gas/Brk |\\n";
tableMd += "|---------|-----------|----------|-------|---------|---------|-------|---------|------------|-----------|-----------------|---------|\\n";
subset.forEach(f => {
const leadStr = f.lead.status ? `${f.lead.dRel}m@${f.lead.vLead}m/s` : "None";
const gasBrk = f.gas_pressed ? "GAS" : (f.brake_pressed ? "BRK" : "-");
tableMd += `| ${f.t.toFixed(2).padStart(5)} | ` +
`${f.v_ego_mph.toFixed(1).padStart(5)} | ` +
`${f.logged_drive.a_target.toFixed(2).padStart(8)} | ` +
`${f.model.a_exp.toFixed(2).padStart(5)} | ` +
`${f.planner.a_chill.toFixed(2).padStart(7)} | ` +
`${f.hem.a_fused.toFixed(2).padStart(7)} | ` +
`${f.hem.w_vision.toFixed(2).padStart(5)} | ` +
`${(f.model.should_stop_exp ? "🛑TRUE" : "false").padEnd(7)} | ` +
`${(f.logged_drive.should_stop ? "🛑TRUE" : "false").padEnd(10)} | ` +
`${(f.hem.should_stop_fused ? "🛑TRUE" : "false").padEnd(9)} | ` +
`${leadStr.padEnd(15)} | ` +
`${gasBrk.padEnd(7)} |\\n`;
});
const fullReport = `# Telemetry Investigation Report (Pure EXP vs HEM Diagnostic)
**Range:** ${startT.toFixed(2)}s to ${endT.toFixed(2)}s (${subset.length} frames @ 20Hz, ${(subset.length * 0.05).toFixed(1)}s duration)
**Logged Drive Mode on Car:** \`${isPureExpRun ? "PURE EXPERIMENTAL MODE" : "CHILL / HEM MODE"}\`
## 1. Executive Summary & Accel Comparisons
- **Actual Commanded In-Drive Accel (a_logged):** min \`${minALogged.toFixed(2)} m/s²\`
- **Vision Model Action Accel (a_exp):** min \`${minAExp.toFixed(2)} m/s²\`
- **Simulated ACC MPC Accel (a_chill):** min \`${minAChill.toFixed(2)} m/s²\`
- **Simulated HEM Fused Accel (a_fused):** min \`${minAFused.toFixed(2)} m/s²\`
- **HEM Vision Authority Range (w_vision):** \`[${minWVision.toFixed(2)}, ${maxWVision.toFixed(2)}]\`
- **Vision Stop Intent Flickers (should_stop_exp toggles):** \`${shouldStopFlicks}\`
- **Lead Target Status Flips:** \`${leadStatusFlicks}\`
## 2. High-Density Replay Telemetry
${tableMd}
## 3. Key Frame Dumps (Start, Peak Braking, End)
\`\`\`json
${JSON.stringify({
frame_start: subset[0],
frame_min_a_logged: subset.reduce((prev, curr) => (curr.logged_drive.a_target < prev.logged_drive.a_target ? curr : prev), subset[0]),
frame_min_a_exp: subset.reduce((prev, curr) => (curr.model.a_exp < prev.model.a_exp ? curr : prev), subset[0]),
frame_end: subset[subset.length - 1]
}, null, 2)}
\`\`\`
`;
navigator.clipboard.writeText(fullReport).then(() => {
alert(`Copied ${subset.length} frames (${startT}s - ${endT}s) of complete diagnostic telemetry to clipboard! You can paste it directly into your chat with AI.`);
});
}
</script>
</body>
</html>
"""
class HEMServerHandler(http.server.SimpleHTTPRequestHandler):
frames_data: dict[str, Any] = {}
def do_GET(self):
parsed = urllib.parse.urlparse(self.path)
if parsed.path == "/" or parsed.path == "/index.html":
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(INDEX_HTML.encode("utf-8"))
elif parsed.path == "/api/data":
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(self.frames_data).encode("utf-8"))
else:
self.send_response(404)
self.end_headers()
def start_server(port: int, data_payload: dict[str, Any]):
HEMServerHandler.frames_data = data_payload
socketserver.TCPServer.allow_reuse_address = True
with socketserver.TCPServer(("127.0.0.1", port), HEMServerHandler) as httpd:
url = f"http://127.0.0.1:{port}"
print(f"\n[HEM Inspector] UI running live at: {url}")
threading.Timer(0.8, lambda: webbrowser.open(url)).start()
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\n[HEM Inspector] Server stopped.")
def main(argv=None):
parser = argparse.ArgumentParser(description="HEM Live Inspector & Diagnostics")
parser.add_argument("route", help="Route identifier (e.g. dongle|2023-07-27--13-01-19 or dongle/log_id/segment)")
parser.add_argument("--segment", type=int, default=0, help="Segment index")
parser.add_argument("--start", type=float, default=None, help="Start time in seconds (segment-relative)")
parser.add_argument("--end", type=float, default=None, help="End time in seconds (segment-relative)")
parser.add_argument("--port", type=int, default=8090, help="Web server port (default 8090)")
parser.add_argument("--data_dir", default=None, help="Local directory containing routes")
parser.add_argument("--bias", type=float, default=0.0, help="Override hybrid_exp_bias")
parser.add_argument("--sensitivity", type=float, default=1.0, help="Override hybrid_vision_brake_sensitivity")
args = parser.parse_args(argv)
toggles = SimpleNamespace(
hybrid_exp_bias=args.bias,
hybrid_vision_brake_sensitivity=args.sensitivity,
taco_tune=False,
classic_model=False,
tinygrad_model=False,
vEgoStopping=0.05,
radar_takeoffs=False,
lane_change_close_gap=False,
minimum_lane_change_speed=0.0,
model_version=None,
)
bufs = load_route_data(args.route, args.segment, args.data_dir)
if bufs is None:
return 1
print("[HEM Inspector] Replaying route segment through HEM & Longitudinal Planner...")
frames, stop_events = run_hem_replay(bufs, args.start, args.end, toggles)
print(f"[HEM Inspector] Processed {len(frames)} frames. Identified {len(stop_events)} stop/decel event candidates.")
start_server(args.port, {"frames": frames, "events": stop_events})
return 0
if __name__ == "__main__":
raise SystemExit(main())
+152 -40
View File
@@ -1,50 +1,162 @@
openpilot in simulator
=====================
# StarPilot Live MetaDrive Simulator
sudo apt install libnvidia-gl-610 # match the KMD version shown by nvidia-smi (610.88)
cd /home/prabh/Projects/openpilot/.host_runtime/linux/worktree && uv pip install --python .venv/bin/python3 PyOpenGL cuda-python 2>&1 | tail -15
openpilot implements a [bridge](run_bridge.py) that allows it to run in the [MetaDrive simulator](https://github.com/metadriverse/metadrive).
Runs the **entire** openpilot stack (`manager.py` -> modeld/controlsd/plannerd/locationd)
on the host PC, bridged to a **straight-road MetaDrive world**, so you can drive the
various longitudinal modes live and watch them behave.
## Launching openpilot
First, start openpilot.
``` bash
# Run locally
./tools/sim/launch_openpilot.sh
This is a StarPilot-specific wrapper. Upstream docs: `tools/sim/launch_openpilot.sh` +
`run_bridge.py` (stock MetaDrive bridge).
---
## Quick start
```bash
# one command, pick a mode:
./tools/sim/starpilot_sim.sh hem # Hybrid Experimental Mode
./tools/sim/starpilot_sim.sh exp # standard Experimental mode
./tools/sim/starpilot_sim.sh chill # pure Chill / CCM mode
./tools/sim/starpilot_sim.sh cem # Conditional Experimental Mode
# extra flags (order doesn't matter):
./tools/sim/starpilot_sim.sh hem --headless # no UI window
./tools/sim/starpilot_sim.sh hem --joystick # use a game wheel instead of keyboard
./tools/sim/starpilot_sim.sh hem --cpu # force CPU backend for the model
./tools/sim/starpilot_sim.sh hem --pkl # use the precompiled pickle instead of tracing the ONNX live
```
## Bridge usage
```
$ ./run_bridge.py -h
usage: run_bridge.py [-h] [--joystick] [--high_quality] [--dual_camera]
Bridge between the simulator and openpilot.
Run it from a **real terminal with a display** (that's where the UI renders and the
keyboard controls work). Press `q` to exit.
options:
-h, --help show this help message and exit
--joystick
--high_quality
--dual_camera
### What each mode does
| command | toggle set |
|-----------|-----------------------------------------------------------------------------|
| `exp` | `ConditionalExperimental=off, ConditionalChill=off, HybridExperimental=off` (full-time experimental) |
| `chill` | `ConditionalChill=on` (full-time Chill / ACC) |
| `cem` | `ConditionalExperimental=on` (conditional experimental) |
| `hem` | `HybridExperimental=on` (hybrid experimental) |
These are mutually exclusive; the launcher sets exactly one.
---
## Prerequisites
- Host build already provisioned under `.host_runtime/linux/worktree` (the launcher
runs `./dev sync` to refresh it from this repo).
- `metadrive-simulator` installed in the host worktree venv (it is, as a dependency).
- The driving model in the model store: `~/.comma/starpilot/data/models/rdf43_driving_tinygrad.pkl`.
This is a PC (CUDA) build compiled from `driving_supercombo.onnx` (also kept in the model
store as `driving_supercombo.onnx`). The launcher copies the pkl into the worktree where
`modeld` expects it on every launch.
- An NVIDIA GPU with working tinygrad **CUDA** (default model device).
## How the model is built
**Default (Option 1, live-ONNX):** `modeld` loads `driving_supercombo.onnx` directly and
traces it in-memory on the RTX at startup — `OnnxRunner` + `TinyJit` captured live during the
first frames. There is **no** tinygrad-JIT pickle round-trip, so the JIT-unpickler bug (which
rewrites kernel targets to the comma-device default `QCOM`) is never triggered. This is the
reliable path on a PC host. Set `STARPIOT_LIVE_ONNX=<path-to>.onnx` (the sim launcher does
this for you) to enable it; `STARPIOT_MODEL_SIZE` defaults to `512x256`.
**Fallback (--pkl):** a precompiled tinygrad-JIT pickle, built for the PC CUDA backend:
```bash
./dev python selfdrive/modeld/compile_modeld.py \
--model-type supercombo --model-size 512x256 --camera-resolutions 1928x1208 \
--supercombo-onnx ~/.comma/starpilot/data/models/driving_supercombo.onnx \
--behavior-version v15 \
--output ~/.comma/starpilot/data/models/rdf43_driving_tinygrad.pkl
```
#### Bridge Controls:
- To engage openpilot press 2, then press 1 to increase the speed and 2 to decrease.
- To disengage, press "S" (simulates a user brake)
`compile_modeld.py` now skips its JIT pickle round-trip by default (`STARPIOT_DO_JIT_ROUNDTRIP`
re-enables it); that round-trip rewrites compiled device/target refs inside the JIT to the
comma-device default (`QCOM`), producing an artifact that cannot run on a PC CUDA host.
Because the pkl path still goes through this fork's buggy JIT unpickler at load time, prefer
the live-ONNX path.
#### All inputs:
## GPU notes
- **Model** runs on the RTX via tinygrad **CUDA** (`DEV=CUDA`, `STARPIOT_SIM_DEV=CUDA`).
The whole model — camera warp **and** policy — is compiled for CUDA (`WARP_DEV=CUDA`).
Pass `--cpu` to force the CPU backend instead.
### Low MetaDrive FPS / "not using the GPU" (read this)
Two separate things use the GPU in this sim:
1. **tinygrad model** — already CUDA on the RTX (works, verified). Unaffected by the below.
2. **MetaDrive world + camera rendering** (Panda3D). This is the part that was slow.
**Verified diagnosis on this laptop:** the RTX 4050 is **compute-only** — `nvidia-smi` shows
`Disp.A: Off`, 0 MiB — so it is *not* driving the display. MetaDrive renders through Panda3D's
`glxGraphicsPipe`, which runs on the display's GL context. On this host that resolves to
**Mesa llvmpipe software** rendering (checked with `glGetString(GL_RENDERER)` →
`llvmpipe (LLVM 20.1.2)`), i.e. **no GPU at all**, which is exactly why FPS is low.
- Installing `libnvidia-gl-610` adds the NVIDIA GL userspace libs (`10_nvidia.json`,
`libGLX_nvidia.so`), but it does **not** change the sim's FPS here, because NVIDIA is not
the display GPU and GLX still uses the iGPU/Mesa stack. (PRIME offload
`__NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia` was tried and fails with
"Could not find a usable pixel format".)
- The real fix is making the *display's* GL hardware-accelerated (proper iGPU GLX on `:0`,
or an NVIDIA/EGL offscreen render setup) — a system/graphics-config task, not a repo change.
**CUDA image capture is off by default (and must stay off on non-NVIDIA GL).** Installing
`cupy`/`PyOpenGL`/`cuda-python` flips MetaDrive's `_cuda_enable` to `True`; if `image_on_cuda`
then follows it, `cudaGraphicsGLRegisterImage` fails with `cudaErrorUnknown` on a Mesa GL
context and **crashes MetaDrive at sensor init**. The bridge therefore defaults
`image_on_cuda` to `False` (safe CPU `RTMCopyRam` readback). To opt into CUDA images on a
machine where the GL context genuinely is NVIDIA-backed, set `STARPIOT_CUDA_IMAGES=1`.
The sim's `camerad` RGB→NV12 conversion likewise falls back to CPU numpy when no OpenCL ICD
is present; install `nvidia-opencl-icd` if you want that on-GPU too.
---
## Driving controls (keyboard)
| key | action |
|-----|--------|
| `r` | Reset simulation (back to the start point) |
| `i` | Toggle ignition (**starts ON** by default) |
| `2` | Cruise **Set** — engages openpilot (lateral + longitudinal control) |
| `1` | Cruise Resume / accel |
| `3` | Cruise Cancel |
| `q` | Quit everything |
| `w/a/s/d` | Manual throttle / steer / brake |
### "Go back to start, then turn on lateral + longitudinal control like in the car"
1. Press **`r`** — the world resets and the car respawns at the start of the straight road.
2. Press **`2`** (cruise set) — openpilot engages: **lateral** (steering) and
**longitudinal** (accel/brake) control turn on together, same as hitting the cruise
set button in the car. `1` bumps the set speed up, `3` cancels.
3. The bridge also auto-engages shortly after startup, so you usually just have to sit back
and let it drive the straight road.
> **Ignition is ON by default.** If the status line ever shows `Ignition: False`, press
> `i` once to turn it back on. Note the trap: pressing `i` toggles it *off*, so don't press
> it at startup unless you want to switch it off.
---
## Status / known blocker
With the **live-ONNX** path (the default), `modeld` traces the model in-memory on the RTX and
the old QCOM-rewrite blocker is bypassed entirely — there is no JIT pickle to unpickle, so
`CUDA_ERROR_INVALID_IMAGE` from a corrupted camera-warp kernel no longer applies. If you run
with `--pkl` instead, the pickle path still hits this fork's buggy JIT unpickler at load time
(rewrites kernel targets to `QCOM::a630`, then the warp recompiles as a QCOM image), which is
exactly why Option 1 is the default. Fixing the pkl path would require a small change in the
vendored tinygrad to preserve the target device across JIT unpickle.
For a working stop-sign reproduction today, use the replay forensics on a real route:
```bash
./dev python tools/replay/hem_forensic.py <dongleId>/<routeId> --segments 0,1
./dev python tools/replay/mode_sim.py <dongleId>/<routeId> --segment 0
```
| key | functionality |
|------|-----------------------|
| 1 | Cruise Resume / Accel |
| 2 | Cruise Set / Decel |
| 3 | Cruise Cancel |
| r | Reset Simulation |
| i | Toggle Ignition |
| q | Exit all |
| wasd | Control manually |
```
## MetaDrive
### Launching Metadrive
Start bridge processes located in tools/sim:
``` bash
./run_bridge.py
```
+63 -6
View File
@@ -58,6 +58,13 @@ class SimulatorBridge(ABC):
self.past_startup_engaged = False
self.startup_button_prev = True
self.startup_set_count = 0
self.auto_engage_speed_done = False
self.AUTO_CRUISE_KPH = 60.0
self.manual_throttle = 0.0
self.manual_brake = 0.0
self.manual_steer = 0.0
self.test_run = False
@@ -126,7 +133,9 @@ Ignition: {self.simulator_state.ignition} Engaged: {self.simulator_state.is_enga
self.simulator_state.left_blinker = False
self.simulator_state.right_blinker = False
throttle_manual = steer_manual = brake_manual = 0.
throttle_manual = self.manual_throttle
steer_manual = self.manual_steer
brake_manual = self.manual_brake
# Read manual controls
if not q.empty():
@@ -134,11 +143,11 @@ Ignition: {self.simulator_state.ignition} Engaged: {self.simulator_state.is_enga
if message.type == QueueMessageType.CONTROL_COMMAND:
m = message.info.split('_')
if m[0] == "steer":
steer_manual = float(m[1])
steer_manual = self.manual_steer = float(m[1])
elif m[0] == "throttle":
throttle_manual = float(m[1])
throttle_manual = self.manual_throttle = float(m[1])
elif m[0] == "brake":
brake_manual = float(m[1])
brake_manual = self.manual_brake = float(m[1])
elif m[0] == "cruise":
if m[1] == "down":
self.simulator_state.cruise_button = CruiseButtons.DECEL_SET
@@ -146,6 +155,7 @@ Ignition: {self.simulator_state.ignition} Engaged: {self.simulator_state.is_enga
self.simulator_state.cruise_button = CruiseButtons.RES_ACCEL
elif m[1] == "cancel":
self.simulator_state.cruise_button = CruiseButtons.CANCEL
self.manual_throttle = self.manual_brake = self.manual_steer = 0.0
elif m[1] == "main":
self.simulator_state.cruise_button = CruiseButtons.MAIN
elif m[0] == "blinker":
@@ -173,14 +183,26 @@ Ignition: {self.simulator_state.ignition} Engaged: {self.simulator_state.is_enga
self.simulator_state.is_engaged = self.simulated_car.sm['selfdriveState'].active
if self.simulator_state.is_engaged:
self.manual_throttle = self.manual_brake = self.manual_steer = 0.0
throttle_op = np.clip(self.simulated_car.sm['carControl'].actuators.accel / 1.6, 0.0, 1.0)
brake_op = np.clip(-self.simulated_car.sm['carControl'].actuators.accel / 4.0, 0.0, 1.0)
steer_op = self.simulated_car.sm['carControl'].actuators.steeringAngleDeg
# After auto-engaging at standstill the set speed is captured at ~floor
# (a crawl). Ramp it up to a real driving speed via cruise-accel presses.
if not self.auto_engage_speed_done and self.rk.frame % 5 == 0:
if self.simulated_car.sm['carState'].vCruise < self.AUTO_CRUISE_KPH:
self.simulator_state.cruise_button = CruiseButtons.RES_ACCEL
else:
self.auto_engage_speed_done = True
self.past_startup_engaged = True
elif not self.past_startup_engaged and self.simulated_car.sm['selfdriveState'].engageable:
self.simulator_state.cruise_button = CruiseButtons.DECEL_SET if self.startup_button_prev else CruiseButtons.MAIN # force engagement on startup
self.startup_button_prev = not self.startup_button_prev
# Auto-engage whenever the car is ready. Cruise-Set (DECEL_SET) engages from
# standstill; pressing periodically (not every frame) avoids ratcheting the set
# speed down while still catching the moment the car becomes engageable.
if self.rk.frame % 10 == 0:
self.simulator_state.cruise_button = CruiseButtons.DECEL_SET
throttle_out = throttle_op if self.simulator_state.is_engaged else throttle_manual
brake_out = brake_op if self.simulator_state.is_engaged else brake_manual
@@ -190,6 +212,41 @@ Ignition: {self.simulator_state.ignition} Engaged: {self.simulator_state.is_enga
self.world.read_state()
self.world.read_sensors(self.simulator_state)
if self.rk.frame % 300 == 0:
try:
from PIL import Image
cam = getattr(self.world, 'road_image', None)
if cam is not None and getattr(cam, 'any', None) and cam.any():
Image.fromarray(cam[:, :, ::-1]).save(f"/tmp/kilo/cam_{self.rk.frame}.jpg", quality=90)
except Exception as e:
print(f"CAM-ERR {e}", flush=True)
if self.rk.frame % 25 == 0 and not self.test_run:
try:
st = self.simulator_state
eng = st.is_engaged
engable = self.simulated_car.sm['selfdriveState'].engageable
active = self.simulated_car.sm['selfdriveState'].active
accel = self.simulated_car.sm['carControl'].actuators.accel if eng else float('nan')
cs = self.simulated_car.sm['carState']
sp = self.simulated_car.sm['starpilotPlan']
rd = self.simulated_car.sm['radarState']
m = self.simulated_car.sm['modelV2']
lead = rd.leadOne
print(f"BRIDGE engaged={eng} engable={engable} active={active} accel={accel:.2f} "
f"gas={throttle_out:.2f} brake={brake_out:.2f} steer={steer_out:.1f} "
f"v={st.speed if st.velocity is not None else -1:.2f} "
f"pos=({st.position[0]:.1f},{st.position[1]:.1f}) yaw={st.bearing:.1f} vCruiseK={cs.vCruise:.0f} "
f"forcStop={sp.forcingStop} apprStop={sp.approachStopLength:.1f} "
f"modelLen={m.position.x[-1]:.0f} mVel={m.velocity.x[-1]:.1f} "
f"mAcc={m.acceleration.x[-1]:.2f} sign={sp.stopSignConfirmed} "
f"leadD={lead.dRel:.1f} leadV={lead.vLead:.1f}", flush=True)
except Exception as e:
import traceback; traceback.print_exc()
print(f"BRIDGE-ERR {type(e).__name__}: {e}", flush=True)
if self.world.exit_event.is_set():
self.shutdown()
+10 -11
View File
@@ -1,4 +1,5 @@
import math
import os
from multiprocessing import Queue
from metadrive.component.sensors.base_camera import _cuda_enable
@@ -28,21 +29,13 @@ def curve_block(length, angle=45, direction=0):
}
def create_map(track_size=60):
curve_len = track_size * 2
return dict(
type=MapGenerateMethod.PG_MAP_FILE,
lane_num=2,
lane_width=4.5,
config=[
None,
straight_block(track_size),
curve_block(curve_len, 90),
straight_block(track_size),
curve_block(curve_len, 90),
straight_block(track_size),
curve_block(curve_len, 90),
straight_block(track_size),
curve_block(curve_len, 90),
straight_block(track_size * 10),
straight_block(track_size * 20),
]
)
@@ -73,7 +66,13 @@ class MetaDriveBridge(SimulatorBridge):
image_source="rgb_road",
),
sensors=sensors,
image_on_cuda=_cuda_enable,
# CUDA image capture (cudaGraphicsGLRegisterImage) only works when the
# Panda3D GL context is NVIDIA-backed. On hybrid/compute-only laptops the
# display GL runs on the iGPU via Mesa (often llvmpipe software), so CUDA-GL
# interop fails with cudaErrorUnknown and crashes MetaDrive at sensor init.
# Default to the safe CPU readback path; opt into CUDA explicitly only when
# the GL context is actually NVIDIA.
image_on_cuda=bool(os.getenv("STARPIOT_CUDA_IMAGES")) and _cuda_enable,
image_observation=True,
interface_panel=[],
out_of_route_done=False,
@@ -86,6 +86,7 @@ class MetaDriveWorld(World):
curr_pos = md_vehicle.position
state.velocity = md_vehicle.velocity
state.position = md_vehicle.position
state.bearing = md_vehicle.bearing
state.steering_angle = md_vehicle.steering_angle
state.gps.from_xy(curr_pos)
+48 -18
View File
@@ -1,7 +1,5 @@
import numpy as np
import os
import pyopencl as cl
import pyopencl.array as cl_array
from msgq.visionipc import VisionIpcServer, VisionStreamType
from cereal import messaging
@@ -24,17 +22,25 @@ class Camerad:
self.vipc_server.start_listener()
# set up for pyopencl rgb to yuv conversion
self.ctx = cl.create_some_context()
self.queue = cl.CommandQueue(self.ctx)
cl_arg = f" -DHEIGHT={H} -DWIDTH={W} -DRGB_STRIDE={W * 3} -DUV_WIDTH={W // 2} -DUV_HEIGHT={H // 2} -DRGB_SIZE={W * H} -DCL_DEBUG "
kernel_fn = os.path.join(BASEDIR, "tools/sim/rgb_to_nv12.cl")
with open(kernel_fn) as f:
prg = cl.Program(self.ctx, f.read()).build(cl_arg)
self.krnl = prg.rgb_to_nv12
self.Wdiv4 = W // 4 if (W % 4 == 0) else (W + (4 - W % 4)) // 4
self.Hdiv4 = H // 4 if (H % 4 == 0) else (H + (4 - H % 4)) // 4
# GPU-accelerated rgb->nv12 via pyopencl when an OpenCL platform exists,
# otherwise fall back to a CPU numpy conversion (e.g. laptops without an
# OpenCL ICD, such as NVIDIA-only hosts).
self.ctx = None
try:
import pyopencl as cl
import pyopencl.array as cl_array
self._cl_array = cl_array
self.ctx = cl.create_some_context()
self.queue = cl.CommandQueue(self.ctx)
cl_arg = f" -DHEIGHT={H} -DWIDTH={W} -DRGB_STRIDE={W * 3} -DUV_WIDTH={W // 2} -DUV_HEIGHT={H // 2} -DRGB_SIZE={W * H} -DCL_DEBUG "
kernel_fn = os.path.join(BASEDIR, "tools/sim/rgb_to_nv12.cl")
with open(kernel_fn) as f:
prg = cl.Program(self.ctx, f.read()).build(cl_arg)
self.krnl = prg.rgb_to_nv12
self.Wdiv4 = W // 4 if (W % 4 == 0) else (W + (4 - W % 4)) // 4
self.Hdiv4 = H // 4 if (H % 4 == 0) else (H + (4 - H % 4)) // 4
except Exception:
self.ctx = None
def cam_send_yuv_road(self, yuv):
self._send_yuv(yuv, self.frame_road_id, 'roadCameraState', VisionStreamType.VISION_STREAM_ROAD)
@@ -49,11 +55,35 @@ class Camerad:
assert rgb.shape == (H, W, 3), f"{rgb.shape}"
assert rgb.dtype == np.uint8
rgb_cl = cl_array.to_device(self.queue, rgb)
yuv_cl = cl_array.empty_like(rgb_cl)
self.krnl(self.queue, (self.Wdiv4, self.Hdiv4), None, rgb_cl.data, yuv_cl.data).wait()
yuv = np.resize(yuv_cl.get(), rgb.size // 2)
return yuv.data.tobytes()
if self.ctx is not None:
rgb_cl = self._cl_array.to_device(self.queue, rgb)
yuv_cl = self._cl_array.empty_like(rgb_cl)
self.krnl(self.queue, (self.Wdiv4, self.Hdiv4), None, rgb_cl.data, yuv_cl.data).wait()
yuv = np.resize(yuv_cl.get(), rgb.size // 2)
return yuv.data.tobytes()
return self.rgb_to_yuv_cpu(rgb).tobytes()
@staticmethod
def rgb_to_yuv_cpu(rgb):
"""Numpy NV12 conversion mirroring tools/sim/rgb_to_nv12.cl (BT.601 limited)."""
r = rgb[:, :, 0].astype(np.int32)
g = rgb[:, :, 1].astype(np.int32)
b = rgb[:, :, 2].astype(np.int32)
y = ((b * 13 + g * 65 + r * 33 + 64) >> 7) + 16
y = np.clip(y, 0, 255).astype(np.uint8)
def avg2(ch):
return (ch[0::2, 0::2] + ch[0::2, 1::2] + ch[1::2, 0::2] + ch[1::2, 1::2] + 1) >> 1
r2, g2, b2 = avg2(r), avg2(g), avg2(b)
u = ((b2 * 56 - g2 * 37 - r2 * 19 + 0x8080) >> 8) & 0xFF
v = ((r2 * 56 - g2 * 47 - b2 * 9 + 0x8080) >> 8) & 0xFF
uv = np.empty((H // 2, W), dtype=np.uint8)
uv[:, 0::2] = u.astype(np.uint8)
uv[:, 1::2] = v.astype(np.uint8)
return np.concatenate([y.ravel(), uv.ravel()]).astype(np.uint8)
def _send_yuv(self, yuv, frame_id, pub_type, yuv_type):
eof = int(frame_id * 0.05 * 1e9)
+2 -1
View File
@@ -5,7 +5,7 @@ import numpy as np
from abc import ABC, abstractmethod
from collections import namedtuple
W, H = 1928, 1208
W, H = 1164, 874
vec3 = namedtuple("vec3", ["x", "y", "z"])
@@ -41,6 +41,7 @@ class SimulatorState:
self.ignition = True
self.velocity: vec3 = None
self.position: tuple = (0, 0)
self.bearing: float = 0
self.gps = GPSState()
self.imu = IMUState()
+15 -3
View File
@@ -34,7 +34,11 @@ KEYBOARD_HELP = """
def getch() -> str:
STDIN_FD = sys.stdin.fileno()
old_settings = termios.tcgetattr(STDIN_FD)
try:
old_settings = termios.tcgetattr(STDIN_FD)
except (termios.error, OSError):
# No controlling terminal (e.g. headless / background run). Idle instead of crashing.
return None
try:
# set
mode = old_settings.copy()
@@ -48,8 +52,13 @@ def getch() -> str:
termios.tcsetattr(STDIN_FD, termios.TCSAFLUSH, mode)
ch = sys.stdin.read(1)
except (termios.error, OSError):
ch = None
finally:
termios.tcsetattr(STDIN_FD, termios.TCSADRAIN, old_settings)
try:
termios.tcsetattr(STDIN_FD, termios.TCSADRAIN, old_settings)
except (termios.error, OSError):
pass
return ch
def print_keyboard_help():
@@ -60,7 +69,10 @@ def keyboard_poll_thread(q: 'Queue[QueueMessage]'):
while True:
c = getch()
if c == '1':
if c is None:
# No terminal input available (headless/background); avoid busy-looping.
time.sleep(0.05)
elif c == '1':
q.put(control_cmd_gen("cruise_up"))
elif c == '2':
q.put(control_cmd_gen("cruise_down"))
+11 -1
View File
@@ -15,7 +15,7 @@ class SimulatedCar:
def __init__(self):
self.pm = messaging.PubMaster(['can', 'pandaStates'])
self.sm = messaging.SubMaster(['carControl', 'controlsState', 'carParams', 'selfdriveState'])
self.sm = messaging.SubMaster(['carControl', 'controlsState', 'carParams', 'selfdriveState', 'carState', 'starpilotPlan', 'radarState', 'modelV2'])
self.cp = self.get_car_can_parser()
self.idx = 0
self.params = Params()
@@ -76,6 +76,16 @@ class SimulatedCar:
msg.append(self.packer.make_can_msg("STEERING_CONTROL", 2, {}))
msg.append(self.packer.make_can_msg("ACC_HUD", 2, {}))
msg.append(self.packer.make_can_msg("LKAS_HUD", 2, {}))
# 0x35e CAMERA_MESSAGES: StarPilot's Honda carstate registers this message
# when it reads the speed-limit / stop-sign fields (HAS_CAMERA_MESSAGES
# flag), and can_valid requires *every* registered message to be valid.
# The real camera sends it on the cam bus, so publish it here too;
# CANPacker auto-fills the Honda COUNTER/CHECKSUM. SPEED_LIMIT_SIGN=0
# means no posted speed limit, so calculate_speed_limit() returns 0.0.
msg.append(self.packer.make_can_msg("CAMERA_MESSAGES", 2, {
"SPEED_LIMIT_SIGN": 0,
"ROAD_SIGN": 0,
}))
self.pm.send('can', can_list_to_can_capnp(msg))
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env bash
# StarPilot live MetaDrive simulator launcher.
#
# Runs the ENTIRE openpilot stack (manager.py -> modeld/controlsd/plannerd/...) on
# the host RTX GPU, bridged to a straight-road MetaDrive world, in one of three
# longitudinal modes.
#
# Usage:
# ./tools/sim/starpilot_sim.sh {exp|chill|cem|hem} [--headless] [--joystick] [--cpu]
#
# exp : standard Experimental mode (ConditionalExperimental=off, ConditionalChill=off, HybridExperimental=off)
# chill : pure Chill / CCM mode (ConditionalChill=on)
# cem : Conditional Experimental mode (ConditionalExperimental=on)
# hem : Hybrid Experimental mode (HybridExperimental=on)
#
# The model runs on the RTX GPU (tinygrad CUDA) by default. Pass --cpu to force
# the CPU backend instead (fragile in this fork; not recommended).
#
# By default the model is traced LIVE from driving_supercombo.onnx inside modeld
# (Option 1: no tinygrad-JIT pickling, so no JIT-unpickle corruption). Pass --pkl
# to fall back to the precompiled pickle artifact instead.
#
# The stack runs from the isolated host worktree (.host_runtime/linux/worktree),
# which is synced from this repo by `./dev sync` (run automatically below).
set -euo pipefail
# Clean up any stale openpilot/sim processes and shared-memory sockets from a
# previous run. pkill on "manager.py" alone misses the the_galaxy/galaxy Flask
# processes (different proctitles) which otherwise hold port 8083 and crash-loop,
# and stale msgq sockets in /dev/shm collide with fresh publishers.
pkill -9 -f "system/manager/manager.py" 2>/dev/null || true
pkill -9 -f "run_bridge.py" 2>/dev/null || true
pkill -9 -f "metadrive" 2>/dev/null || true
pkill -9 -f "the_galaxy" 2>/dev/null || true
pkill -9 -f "galaxy.galaxy" 2>/dev/null || true
sleep 1
rm -rf /dev/shm/msgq* /dev/shm/visionipc* /tmp/openpilot* 2>/dev/null || true
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
MODE="${1:-hem}"
ARGS=("${@:2}")
HEADLESS=0
JOYSTICK=0
SIM_DEV="CUDA"
LIVE_ONNX=1
for a in "${ARGS[@]}"; do
case "$a" in
--headless) HEADLESS=1 ;;
--joystick) JOYSTICK=1 ;;
--cpu) SIM_DEV="CPU" ;;
--pkl) LIVE_ONNX=0 ;;
*) echo "unknown arg: $a" >&2; exit 1 ;;
esac
done
case "$MODE" in
exp|chill|cem|hem) : ;;
*) echo "usage: $0 {exp|chill|cem|hem} [--headless] [--joystick] [--cpu]" >&2; exit 1 ;;
esac
# 1. Sync main repo -> host worktree so our sim/mode changes land there.
"${ROOT_DIR}/dev" sync shared >/dev/null 2>&1 || true
HOST_WORKTREE="${ROOT_DIR}/.host_runtime/linux/worktree"
HOST_PY="${HOST_WORKTREE}/.venv/bin/python3"
MODEL_STORE="${HOME}/.comma/starpilot/data/models"
echo "==> StarPilot sim mode: ${MODE} (model device: ${SIM_DEV}) (host worktree: ${HOST_WORKTREE})"
# 2. Make sure the builtin model is present where modeld loads it. Sync wipes the
# worktree copy, so re-copy it from the model store on every launch. With the
# live-ONNX path (default) modeld ignores the pkl and traces the ONNX instead,
# but keep the pkl in place so --pkl still works.
MODEL_SRC="${MODEL_STORE}/rdf43_driving_tinygrad.pkl"
MODEL_DST="${HOST_WORKTREE}/selfdrive/modeld/models/driving_tinygrad.pkl"
if [[ -f "${MODEL_SRC}" ]]; then
if ! cmp -s "${MODEL_SRC}" "${MODEL_DST}"; then
cp -f "${MODEL_SRC}" "${MODEL_DST}"
echo "==> Copied builtin model to ${MODEL_DST}"
fi
else
echo "!! builtin model not found at ${MODEL_SRC}" >&2
fi
# Option 1 (live-ONNX): modeld traces driving_supercombo.onnx in-memory on the
# RTX, so the tinygrad-JIT pickle round-trip (and its QCOM-rewrite bug) never
# happens. Falls back to the pkl artifact when --pkl is passed or the onnx is
# missing.
# Locate the source ONNX: prefer the model store copy, fall back to the repo root.
ONNX_SRC="${MODEL_STORE}/driving_supercombo.onnx"
if [[ ! -f "${ONNX_SRC}" ]] && [[ -f "${ROOT_DIR}/driving_supercombo.onnx" ]]; then
ONNX_SRC="${ROOT_DIR}/driving_supercombo.onnx"
fi
if [[ "$LIVE_ONNX" == "1" ]]; then
if [[ -f "${ONNX_SRC}" ]]; then
export STARPIOT_LIVE_ONNX="${ONNX_SRC}"
echo "==> modeld will trace ${ONNX_SRC} live on ${SIM_DEV}"
else
echo "!! driving_supercombo.onnx NOT found (checked model store and repo root)." >&2
echo " Falling back to the precompiled pkl, which CRASHES with CUDA_ERROR_INVALID_IMAGE." >&2
echo " Place the onnx at ${MODEL_STORE}/driving_supercombo.onnx and re-run." >&2
fi
fi
# 3. Set the longitudinal mode params (mutually exclusive).
# ForceOnroad: on a PC host there is no real panda/ignition, so hardwared never
# transitions the device onroad by itself; force it so modeld/controlsd/plannerd
# come up and the sim can engage and drive.
"${HOST_PY}" - "${MODE}" <<'PY'
import sys
from openpilot.common.params import Params
p = Params()
mode = sys.argv[1]
p.put_bool_nonblocking("ConditionalExperimental", mode == "cem")
p.put_bool_nonblocking("ConditionalChill", mode == "chill")
p.put_bool_nonblocking("HybridExperimental", mode == "hem")
p.put_bool_nonblocking("ForceOnroad", True)
print(f"params: ConditionalExperimental={p.get_bool('ConditionalExperimental')} "
f"ConditionalChill={p.get_bool('ConditionalChill')} "
f"HybridExperimental={p.get_bool('HybridExperimental')} "
f"ForceOnroad={p.get_bool('ForceOnroad')}")
PY
# 4. Sim environment (mirrors tools/sim/launch_openpilot.sh) + GPU selection.
export PASSIVE="0"
export NOBOARD="1"
export SIMULATION="1"
export SKIP_FW_QUERY="1"
export FINGERPRINT="HONDA_CIVIC_2022"
export BLOCK="camerad,loggerd,encoderd,micd,logmessaged,soundd,mapd"
if [[ "$HEADLESS" == "1" ]]; then
export BLOCK="${BLOCK},ui"
fi
export DEV="${SIM_DEV}"
export STARPIOT_SIM_DEV="${SIM_DEV}"
# The supercombo artifact is compiled all-CUDA (warp + policy on the RTX). The
# warp device must be CUDA so the precompiled kernels match; modeld copies the
# host-memory camera frames onto the GPU before the warp.
if [[ "${SIM_DEV}" == "CPU" ]]; then
export WARP_DEV="CPU"
export QUEUE_DEV="CPU"
else
export WARP_DEV="${SIM_DEV}"
export QUEUE_DEV="${SIM_DEV}"
fi
# 5. Launch the full openpilot stack in the background, then the bridge in the foreground.
cat <<'HELP'
==> Controls (focus the terminal that launched this script):
i : toggle ignition (starts ON by default; if the status line shows
Ignition: False, press i once to turn it back on)
2 : cruise Set (engage lateral + longitudinal) 1 : cruise Resume / accel
3 : cruise Cancel r : reset simulation
w/a/s/d : manual throttle / steer / brake q : quit everything
z/x : blinker left / right
HELP
cd "${HOST_WORKTREE}"
MANAGER_LOG="${HOST_WORKTREE}/.host_sim_manager.log"
"${HOST_PY}" -c "from openpilot.selfdrive.test.helpers import set_params_enabled; set_params_enabled()"
echo "==> Starting openpilot stack (manager.py) ..."
"${HOST_PY}" system/manager/manager.py >"${MANAGER_LOG}" 2>&1 &
MANAGER_PID=$!
echo "==> manager.py pid ${MANAGER_PID} (log: ${MANAGER_LOG})"
trap 'echo "==> stopping manager (${MANAGER_PID})"; kill "${MANAGER_PID}" 2>/dev/null || true' EXIT
# modeld blocks on the camerad visionipc stream before it starts tracing the
# ONNX, and that stream is published by the bridge. The bridge MUST come up
# promptly or modeld never loads, so keep the startup delay short and let the
# bridge auto-engage once controls report engageable.
sleep 8
# Single-camera mode: MetaDrive must render only the road viewport. Dual-camera
# renders a second wide viewpoint every frame, roughly halving frame rate. The
# driving pipeline (modeld) consumes roadCameraState only, so the wide cam adds
# no control value in sim.
BRIDGE_ARGS=()
if [[ "$JOYSTICK" == "1" ]]; then
BRIDGE_ARGS+=(--joystick)
fi
echo "==> Starting MetaDrive bridge (${BRIDGE_ARGS[*]}) ..."
"${HOST_PY}" tools/sim/run_bridge.py "${BRIDGE_ARGS[@]}" || true
echo "==> Bridge exited."