hem inspector

This commit is contained in:
Prabhaav Pillai
2026-09-01 01:15:57 -04:00
parent 74a15083aa
commit f394bdb46b
+893
View File
@@ -0,0 +1,893 @@
#!/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())