1
0
forked from IQ.Lvbs/IQ.Pilot

IQ.Pilot Prebuilt Release @ ab07000

This commit is contained in:
IQ.Lvbs history cleanup
2026-08-22 23:42:42 -05:00
commit 9f9c9a70cc
3729 changed files with 778697 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
#!/usr/bin/env python3
# console path for the boot/manager tmux: must never die or block; on any error, forward raw.
import os
import re
import signal
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
except Exception:
pass
_COLOR = sys.stdout.isatty() and os.environ.get("NO_COLOR") is None
_DROP = re.compile(
r"(\x1b\[\?\d+[a-z])"
r"|ln: failed to create symbolic link '[^']*(cursor-server|windsurf-server|vscode-server)"
r"|^Last login:"
r"|gbm_create_device\(\d+\): Info:"
r"|No IRQs found for '"
r"|^pid \d+'s (current|new) affinity list:"
r"|kj/filesystem-disk-unix\.c\+\+:\d+: warning: PWD"
)
_CLOUDLOG = re.compile(r"^([\w./+-]+\.(?:cc|cpp|c|h|py)): (.*)$")
_ALREADY = re.compile(r"^\s*(?:\x1b\[[0-9;]*m)?\s*(CRIT|ERR|WARN|info|dbg)\b")
_ERRISH = re.compile(r"not supported|fail|error|invalid|cannot|timed out|timeout|unable", re.I)
def _sev(msg):
return ("\033[1;38;5;203m", " ERR", "\033[1;38;5;210m") if _ERRISH.search(msg) \
else ("\033[38;5;110m", "info", "")
def _restyle(line):
if _DROP.search(line):
return None
if _ALREADY.match(line):
return line
m = _CLOUDLOG.match(re.sub(r"\x1b\[[0-9;]*m", "", line))
if m and _COLOR:
src, msg = m.group(1), m.group(2)
lc, ln, mc = _sev(msg)
body = f"{mc}{msg}\033[0m" if mc else msg
return f"{lc}{ln}\033[0m \033[2m{src}\033[0m {body}"
return line
def _emit(out, buf):
styled = _restyle(buf)
if styled is not None:
out.write(styled + "\r\n"); out.flush()
def main():
out = sys.stdout
buf = ""
pending_cr = False
read = sys.stdin.buffer.read
while True:
try:
ch = read(1)
except Exception:
break
if not ch:
break
try:
c = ch.decode("utf-8", "replace")
if pending_cr:
pending_cr = False
if c == "\n":
_emit(out, buf); buf = ""; continue
out.write(buf + "\r"); out.flush(); buf = "" # bare \r: progress
if c == "\r":
pending_cr = True
elif c == "\n":
_emit(out, buf); buf = ""
else:
buf += c
except Exception:
try:
out.write(buf); out.flush()
except Exception:
pass
buf = ""; pending_cr = False
if pending_cr:
out.write(buf + "\r"); out.flush()
elif buf:
try:
styled = _restyle(buf)
if styled is not None:
out.write(styled); out.flush()
except Exception:
pass
if __name__ == "__main__":
try:
main()
except Exception:
try:
import shutil
shutil.copyfileobj(sys.stdin.buffer, sys.stdout.buffer)
except Exception:
pass

View File

@@ -0,0 +1,32 @@
# IQ.Pilot pretty git wrappers (gpull/gfetch/gsync/greset/gbv). bash + zsh.
# Source from your shell rc: source .../tools/iqpilot/git-pretty.sh
if [ -n "${BASH_SOURCE:-}" ]; then
_iq_gp_src="${BASH_SOURCE[0]}"
elif [ -n "${ZSH_VERSION:-}" ]; then
_iq_gp_src="${(%):-%x}"
else
_iq_gp_src="$0"
fi
_IQ_GIT_PRETTY="$(cd "$(dirname "$_iq_gp_src")" 2>/dev/null && pwd)/git_pretty.py"
unset _iq_gp_src
if [ -z "${_IQ_GP_PY:-}" ]; then
if command -v python3 >/dev/null 2>&1; then _IQ_GP_PY=python3
elif [ -x /usr/bin/python3 ]; then _IQ_GP_PY=/usr/bin/python3
else _IQ_GP_PY=python; fi
fi
_iq_git_pretty() {
command git "$@" 2>&1 | "$_IQ_GP_PY" "$_IQ_GIT_PRETTY"
if [ -n "${ZSH_VERSION:-}" ]; then
return ${pipestatus[1]}
else
return ${PIPESTATUS[0]}
fi
}
gpull() { _iq_git_pretty -c color.ui=always pull --progress "$@"; }
gfetch() { _iq_git_pretty -c color.ui=always fetch --progress "$@"; }
gsync() { _iq_git_pretty -c color.ui=always submodule update --init --recursive --progress "$@"; }
greset() { _iq_git_pretty -c color.ui=always reset "$@"; }
gbv() { _iq_git_pretty -c color.branch=always branch -v "$@"; }

123
tools/iqpilot/git_pretty.py Normal file
View File

@@ -0,0 +1,123 @@
#!/usr/bin/env python3
import os
import re
import sys
_GRAD = {
"PULL": ((95, 240, 150), (40, 200, 120)),
"FETCH": ((95, 205, 255), (130, 110, 250)),
"SUBMOD": ((200, 160, 255), (150, 110, 250)),
"RESET": ((255, 190, 90), (230, 80, 70)),
"BRANCH": ((95, 215, 255), (70, 130, 245)),
}
_TARGET_RGB = (150, 152, 178)
_GREEN = (120, 210, 130)
_DIM = "\033[2;38;5;246m"
_RST = "\033[0m"
def _mode():
if not sys.stdout.isatty() or os.environ.get("NO_COLOR"):
return None
return "true" if os.environ.get("COLORTERM", "").lower() in ("truecolor", "24bit") else "256"
def _fg(rgb, mode):
r, g, b = rgb
if mode == "true":
return f"\033[38;2;{r};{g};{b}m"
if abs(r - g) < 12 and abs(g - b) < 12 and abs(r - b) < 12:
idx = 232 + min(23, round((r + g + b) / 3 / 255 * 23))
else:
idx = 16 + 36 * round(r / 255 * 5) + 6 * round(g / 255 * 5) + round(b / 255 * 5)
return f"\033[38;5;{idx}m"
def _grad(word, label, mode):
start, end = _GRAD.get(label, _GRAD["FETCH"])
n = max(1, len(word) - 1)
out = [f"\033[1m{_fg(tuple(int(s + (e - s) * i / n) for s, e in zip(start, end)), mode)}{ch}"
for i, ch in enumerate(word)]
return "".join(out) + _RST
def _label(label, body, mode):
pad = " " * max(0, 8 - len(label))
return f"{pad}{_grad(label, label, mode)} {_fg(_TARGET_RGB, mode)}{body}{_RST}"
def _restyle(line, mode):
s = line.rstrip("\n")
raw = re.sub(r"\033\[[0-9;]*m", "", s) # match against de-colored text
if raw == "Already up to date.":
return f"{_fg(_GREEN, mode)}✓ already up to date{_RST}"
m = re.match(r"Updating ([0-9a-f]+\.\.[0-9a-f]+)$", raw)
if m:
return _label("PULL", m.group(1), mode)
if raw == "Fast-forward":
return f"{_DIM}fast-forward{_RST}"
m = re.match(r"HEAD is now at ([0-9a-f]+) (.*)$", raw)
if m:
return _label("RESET", f"{m.group(1)} {m.group(2)}", mode)
m = re.match(r"Submodule path '(.+)': checked out '([0-9a-f]+)'$", raw)
if m:
return _label("SUBMOD", f"{m.group(1)} @ {m.group(2)[:9]}", mode)
m = re.match(r"Submodule '(.+)' \((.+)\) registered for path '(.+)'$", raw)
if m:
return _label("SUBMOD", f"{m.group(3)} (registered)", mode)
m = re.match(r"From (.+)$", raw)
if m:
return _label("FETCH", m.group(1), mode)
m = re.match(r"\s*\*?\s*\[new (?:branch|tag)\]\s+(\S+)\s+->\s+(\S+)$", raw)
if m:
return _label("FETCH", f"new {m.group(1)}{m.group(2)}", mode)
m = re.match(r"\s*\*\s+(?:branch|tag)\s+(\S+)\s+->\s+(\S+)$", raw)
if m:
return _label("FETCH", f"{m.group(1)}{m.group(2)}", mode)
m = re.match(r"\s*([0-9a-f]+\.\.[0-9a-f]+)\s+(\S+)\s+->\s+(\S+)$", raw)
if m:
return _label("FETCH", f"{m.group(1)} {m.group(2)}{m.group(3)}", mode)
m = re.match(r"([* ]) +(\S+) +([0-9a-f]{7,})( .*)?$", raw)
if m:
cur, name, sha, msg = m.groups()
star = f"{_fg(_GREEN, mode)}{_RST} " if cur == "*" else " "
return f"{star}{_grad(name, 'BRANCH', mode)} {_fg(_TARGET_RGB, mode)}{sha}{(msg or '')}{_RST}"
return s
def main():
mode = _mode()
out = sys.stdout
if mode is None: # not a tty / NO_COLOR: passthrough
for chunk in iter(lambda: sys.stdin.buffer.read(4096), b""):
out.buffer.write(chunk)
out.buffer.flush()
return
buf = ""
stream = sys.stdin
while True:
ch = stream.read(1)
if not ch:
break
if ch == "\r": # progress fragment: emit live, untouched
out.write(buf + "\r")
out.flush()
buf = ""
elif ch == "\n":
out.write(_restyle(buf, mode) + "\n")
out.flush()
buf = ""
else:
buf += ch
if buf:
out.write(_restyle(buf, mode))
out.flush()
if __name__ == "__main__":
try:
main()
except (BrokenPipeError, KeyboardInterrupt):
pass

753
tools/iqpilot/mici_preview.py Executable file
View File

@@ -0,0 +1,753 @@
#!/usr/bin/env python3
"""
╔══════════════════════════════════════════════════════════════════════════╗
║ IQ.Pilot MICI UI Preview Tool ║
║ ────────────────────────────────────────────────────────────────────── ║
║ Renders any MICI layout/widget at 536×240 on your Mac desktop so you ║
║ can visually inspect and iterate without a physical comma 4. ║
║ ║
║ MODES ║
║ screenshot — render N frames, save PNG(s), open in Preview ║
║ video — render N seconds to MP4 via ffmpeg, open in QuickTime ║
║ live — interactive window, hot-reload on file-save ║
║ ║
║ USAGE ║
║ python tools/iqpilot/mici_preview.py [OPTIONS] ║
║ ║
║ OPTIONS ║
║ --panel PANEL Panel to render: steering, visuals, display, ║
║ software, cruise, trips, osm, models, toggles, ║
║ device, developer, home, settings (default: steering)║
║ --mode MODE screenshot | video | live (default: screenshot) ║
║ --frames N Frames to settle before screenshot (default: 90) ║
║ --shots N Number of screenshots to take (default: 1) ║
║ --duration S Video duration in seconds (default: 4) ║
║ --fps N Render FPS (default: 60) ║
║ --scale F Window scale multiplier (2.0 = 1072×480 window) ║
║ (default: 2.5) ║
║ --out PATH Output file/dir (default: /tmp/mici_preview/) ║
║ --open Open output file(s) after capture (default: True) ║
║ --mock Use mock UI state (no real params needed) ║
║ ║
║ EXAMPLES ║
║ # Screenshot the steering panel (scaled 2.5×, opens in Preview) ║
║ python tools/iqpilot/mici_preview.py --panel steering ║
║ ║
║ # 4-second video of the neon glow animation ║
║ python tools/iqpilot/mici_preview.py --panel steering --mode video ║
║ ║
║ # Live interactive window with hot-reload ║
║ python tools/iqpilot/mici_preview.py --panel visuals --mode live ║
║ ║
║ # Multiple panels in one go (screenshots) ║
║ python tools/iqpilot/mici_preview.py --panel all ║
╚══════════════════════════════════════════════════════════════════════════╝
"""
import argparse
import importlib
import os
import subprocess
import sys
import time
import queue
import threading
from pathlib import Path
# ── Project root on sys.path ──────────────────────────────────────────────
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
# Force MICI mode (no BIG UI)
os.environ.setdefault("BIG", "0")
os.environ.setdefault("IQPILOT_UI", "1")
# Use offscreen mode for screenshot/video (no FPS cap → fast capture)
# Live mode overrides this below.
import pyray as rl # noqa: E402 — must come after env setup
# ── MICI canvas dimensions ─────────────────────────────────────────────────
MICI_W = 536
MICI_H = 240
# ── Output directory ───────────────────────────────────────────────────────
DEFAULT_OUT = Path("/tmp/mici_preview")
# ── Color palette for the preview chrome ──────────────────────────────────
CHROME_BG = rl.Color(18, 18, 22, 255) # dark bg behind the MICI canvas
LABEL_COLOR = rl.Color(160, 160, 160, 200) # panel label text
# ── All available panels ───────────────────────────────────────────────────
ALL_PANELS = [
"steering", "visuals", "display", "software", "cruise",
"trips", "osm", "models", "toggles", "device", "developer", "home",
]
# ─────────────────────────────────────────────────────────────────────────────
# Mock UI state so we can run without a live comma process
# ─────────────────────────────────────────────────────────────────────────────
def _patch_mock_state():
"""Replace real ui_state and Params with lightweight mocks."""
import types
# ── Shared in-memory store ────────────────────────────────────────────────
_store: dict = {}
class MockParams:
"""
Drop-in Params mock that also supports `Params | None` type expressions
at module import time by implementing __or__ / __ror__ on the class itself.
"""
# Allow `Params | None` as a runtime type-union expression
def __class_getitem__(cls, item):
return cls
def __or__(cls, other):
import types as _t
return _t.UnionType if hasattr(_t, 'UnionType') else object
__ror__ = __or__
def __init__(self, _=None, **__):
pass # ignore any constructor args (real Params accepts path kwarg)
def get(self, key, default=None, return_default=False):
val = _store.get(key, default)
# Real Params.get() returns str or None; convert bools → str
if isinstance(val, bool):
return str(int(val))
return val
def get_bool(self, key, default=False):
val = _store.get(key, default)
if isinstance(val, str):
return val not in ('', '0', 'false', 'False', 'None')
return bool(val)
def put(self, key, val):
_store[key] = val
def put_bool(self, key, val):
_store[key] = bool(val)
def put_nonblocking(self, key, val):
_store[key] = val
# Seed some sensible defaults so widgets render realistically
mp = MockParams()
# ── Steering ────────────────────────────────────────────────────────────
mp.put("AolEnabled", False)
mp.put("AolSteeringMode", 0) # 0=remain active, 1=pause, 2=disengage
mp.put("AolMainCruiseAllowed", True)
mp.put("AolUnifiedEngagementMode", False)
mp.put("NeuralNetworkFeedForward", False)
mp.put("IQLaneChangeTimer", 0) # nudge
mp.put("IQLaneChangeBsmDelay", False)
# ── Visuals (correct param keys matching visuals.py) ─────────────────────
mp.put("IQBlindSpotAlerts", True)
mp.put("IQSteerEffortArc", True)
mp.put("IQRoadNameOverlay", True)
mp.put("IQBlinkerIndicators", True)
mp.put("IQAccelMeter", False)
mp.put("IQLeadReadouts", 0) # 0=off
mp.put("IQDevUIInfo", 0) # 0=off
mp.put("AlphaLongitudinalEnabled", False) # real param; gates ChevronInfo
# ── Display ───────────────────────────────────────────────────────────────
mp.put("OnroadScreenOffBrightness", 0) # 0=auto
mp.put("OnroadScreenOffTimer", 60) # 1m
mp.put("InteractivityTimeout", 0) # default
# ── Software ──────────────────────────────────────────────────────────────
mp.put("DisableUpdates", False)
mp.put("GitBranch", "master-mici")
mp.put("Version", "IQ.Pilot 0.9.5-mici")
# ── Models ────────────────────────────────────────────────────────────────
mp.put("IQLiveSteerDelay", False)
mp.put("IQLaneTurnDesire", False)
mp.put("IQLaneTurnValue", "19.0")
# ── Cruise ────────────────────────────────────────────────────────────────
mp.put("ExperimentalMode", False)
mp.put("IQDynamicMode", False)
mp.put("LongitudinalPersonality", 1) # 0=aggressive,1=standard,2=relaxed,3=stock
mp.put("IQSpeedAssistMode", 0) # 0=off
# ── Misc / system ─────────────────────────────────────────────────────────
mp.put("IsMetric", False)
mp.put("UIAccentColor", "#00FFF5") # default neon cyan
# Inject mock into common.params
# IMPORTANT: inject the CLASS (not an instance) so that `Params | None`
# type-union expressions in downstream modules work at import time.
mock_module = types.ModuleType("openpilot.common.params")
mock_module.Params = MockParams
sys.modules["openpilot.common.params"] = mock_module
# ── Mock ui_state (both the iqpilot layer and the top-level selfdrive layer) ─
class MockCP:
enableBsm = True
openpilotLongitudinalControl = True
alphaLongitudinalAvailable = False
# Minimal mock for ui_state.sm — returns empty/default objects for any key
class _MockBundle:
internalName = "mock-model"
displayName = "Mock Model"
index = 0
status = None # not downloading
models = []
overrides = []
class _MockModelManager:
availableBundles = []
activeBundle = _MockBundle()
selectedBundle = None # None = not downloading
class _MockSM:
"""Minimal SubMaster-like dict that returns sensible defaults."""
_data = {
"iqModelManager": _MockModelManager(),
}
def __getitem__(self, key):
return self._data.get(key, type("Empty", (), {"enabled": False})())
@property
def updated(self):
return type("U", (), {"__getitem__": lambda s, k: False})()
@property
def alive(self):
return type("A", (), {"__getitem__": lambda s, k: True})()
@property
def valid(self):
return type("V", (), {"__getitem__": lambda s, k: True})()
@property
def frame(self): return 0
def __contains__(self, key): return True
class MockUIState:
CP = MockCP()
params = mp
sm = _MockSM()
started = False
ignition = False
is_metric = False
has_longitudinal_control = True
always_on_dm = False
recording_audio = False
personality = 1 # standard
custom_interactive_timeout = 0
light_sensor = -1.0
is_release = False
# IQ-specific extras
aol_enabled = False
aol_state = 0
def is_offroad(self): return True
def is_onroad(self): return False
def add_offroad_transition_callback(self, cb): pass
def add_engaged_transition_callback(self, cb): pass
def update_params(self): pass
@property
def engaged(self): return False
_mock_ui_state = MockUIState()
# openpilot.selfdrive.ui.ui_state (base openpilot layer — hosts the IQ UI state classes/enums)
from enum import Enum, IntEnum
class _UIStatus(Enum):
DISENGAGED = "disengaged"
ENGAGED = "engaged"
OVERRIDE = "override"
LAT_ONLY = "lat_only"
LONG_ONLY = "long_only"
class _OnroadTimerStatus(Enum):
NONE = 0
PAUSE = 1
RESUME = 2
class _OnroadBrightness(IntEnum):
AUTO = 0
AUTO_DARK = 1
mock_base_ui_mod = types.ModuleType("openpilot.selfdrive.ui.ui_state")
mock_base_ui_mod.ui_state = _mock_ui_state
mock_base_ui_mod.UIStatus = _UIStatus
mock_base_ui_mod.OnroadTimerStatus = _OnroadTimerStatus
mock_base_ui_mod.OnroadBrightness = _OnroadBrightness
mock_base_ui_mod.device = type("MockDevice", (), {"awake": True})()
sys.modules["openpilot.selfdrive.ui.ui_state"] = mock_base_ui_mod
return mp
# ─────────────────────────────────────────────────────────────────────────────
# Panel loader
# ─────────────────────────────────────────────────────────────────────────────
def load_panel(name: str):
"""
Instantiate a MICI panel/layout by name.
Returns a Widget instance with set_rect() already called.
"""
rect = rl.Rectangle(0, 0, MICI_W, MICI_H)
layouts = {
"steering": ("openpilot.iqpilot.ui.mici.layouts.steering", "SteeringLayoutMici"),
"visuals": ("openpilot.iqpilot.ui.mici.layouts.visuals", "VisualsLayoutMici"),
"display": ("openpilot.iqpilot.ui.mici.layouts.display", "DisplayLayoutMici"),
"software": ("openpilot.iqpilot.ui.mici.layouts.software", "SoftwareLayoutMici"),
"cruise": ("openpilot.iqpilot.ui.mici.layouts.cruise", "CruiseLayoutMici"),
"trips": ("openpilot.iqpilot.ui.mici.layouts.trips", "TripsLayoutMici"),
"osm": ("openpilot.iqpilot.ui.mici.layouts.osm", "OSMLayoutMici"),
"models": ("openpilot.iqpilot.ui.mici.layouts.models", "ModelsLayoutMici"),
"toggles": ("openpilot.selfdrive.ui.mici.layouts.settings.toggles", "TogglesLayoutMici"),
"device": ("openpilot.selfdrive.ui.mici.layouts.settings.device", "DeviceLayoutMici"),
"developer":("openpilot.selfdrive.ui.mici.layouts.settings.developer","DeveloperLayoutMici"),
"home": ("openpilot.selfdrive.ui.mici.layouts.home", "MiciHomeLayout"),
"settings": ("openpilot.iqpilot.ui.mici.layouts.settings", "IQMiciSettingsLayout"),
}
if name not in layouts:
raise ValueError(f"Unknown panel '{name}'. Choose from: {', '.join(layouts)}")
# All IQ.Pilot MICI layout panels accept an optional back_callback
NEEDS_BACK_CB = {
"steering", "visuals", "display", "software", "cruise",
"trips", "osm", "models", "toggles", "device", "developer",
}
mod_path, cls_name = layouts[name]
mod = importlib.import_module(mod_path)
cls = getattr(mod, cls_name)
if name in NEEDS_BACK_CB:
widget = cls(back_callback=lambda: None)
else:
widget = cls()
widget.set_rect(rect)
widget.show_event()
return widget
# ─────────────────────────────────────────────────────────────────────────────
# Rendering helpers
# ─────────────────────────────────────────────────────────────────────────────
def _draw_chrome(panel_name: str, scale: float, canvas_x: int, canvas_y: int):
"""Draw the preview window chrome: background, label, dimension hint."""
# Nothing to draw outside the canvas — the window IS the canvas (+ padding)
pass
def _render_frame(widget, render_tex: rl.RenderTexture, canvas_x: int, canvas_y: int, scale: float, panel_name: str):
"""
Render one frame:
1. Draw the MICI widget into render_tex (536×240 offscreen)
2. Blit the texture into the window at the correct position + scale
3. Draw chrome overlays (panel label, grid, etc.)
"""
# Draw into the MICI-sized render texture
rl.begin_texture_mode(render_tex)
rl.clear_background(rl.Color(0, 0, 0, 255))
widget.render(rl.Rectangle(0, 0, MICI_W, MICI_H))
rl.end_texture_mode()
# Blit to screen (flip Y because OpenGL textures are upside-down)
src = rl.Rectangle(0, 0, MICI_W, -MICI_H) # negative H = flip
dst = rl.Rectangle(canvas_x, canvas_y, MICI_W * scale, MICI_H * scale)
rl.draw_texture_pro(render_tex.texture, src, dst, rl.Vector2(0, 0), 0, rl.WHITE)
# Panel name label bottom-left
rl.draw_text(panel_name.upper(), canvas_x + 6, canvas_y + int(MICI_H * scale) + 6,
14, rl.Color(120, 120, 120, 180))
# Dimension hint bottom-right
hint = f"{MICI_W}×{MICI_H} (×{scale:.1f})"
hint_w = rl.measure_text(hint, 12)
win_w = rl.get_screen_width()
rl.draw_text(hint, win_w - hint_w - 8, canvas_y + int(MICI_H * scale) + 6,
12, rl.Color(80, 80, 80, 160))
# ─────────────────────────────────────────────────────────────────────────────
# Screenshot mode
# ─────────────────────────────────────────────────────────────────────────────
def run_screenshot(panel_name: str, args, out_dir: Path) -> list[Path]:
"""
Render `args.frames` frames (so animations settle), then take `args.shots`
screenshots 0.5s apart. Returns list of saved PNG paths.
Uses gui_app.init_window() so fonts are loaded correctly, and the SCALE
env var (set to args.scale in main()) controls window size.
"""
from openpilot.system.ui.lib.application import gui_app
gui_app.init_window(f"MICI Preview — {panel_name}", fps=args.fps)
# Offscreen MICI-resolution render texture
render_tex = rl.load_render_texture(MICI_W, MICI_H)
# Load widget AFTER window/fonts are initialized
widget = load_panel(panel_name)
saved: list[Path] = []
frame = 0
shots_taken = 0
next_shot_frame = args.frames # first shot after settle
# Display at native scaled window coords (0,0 → screen size)
win_w = rl.get_screen_width()
win_h = rl.get_screen_height()
while not rl.window_should_close() and shots_taken < args.shots:
rl.begin_drawing()
rl.clear_background(CHROME_BG)
# Render widget into the MICI-sized texture, then blit full-window
rl.begin_texture_mode(render_tex)
rl.clear_background(rl.BLACK)
widget.render(rl.Rectangle(0, 0, MICI_W, MICI_H))
rl.end_texture_mode()
# Blit the render texture to fill the whole window (Y-flipped)
src = rl.Rectangle(0, 0, MICI_W, -MICI_H)
dst = rl.Rectangle(0, 0, win_w, win_h)
rl.draw_texture_pro(render_tex.texture, src, dst, rl.Vector2(0, 0), 0, rl.WHITE)
# Settle progress bar
if frame < args.frames:
pct = frame / args.frames
rl.draw_rectangle(0, win_h - 3, int(win_w * pct), 3, rl.Color(0, 255, 245, 140))
rl.draw_text(f"settling {frame}/{args.frames}", 6, 6, 11, rl.Color(100, 100, 100, 160))
rl.end_drawing()
frame += 1
if frame == next_shot_frame:
# Read from render texture (native MICI res, no padding to crop)
img = rl.load_image_from_texture(render_tex.texture)
rl.image_flip_vertical(img)
# Save at 2× native for clarity on Retina displays
rl.image_resize(img, MICI_W * 2, MICI_H * 2)
out_dir.mkdir(parents=True, exist_ok=True)
ts = int(time.time() * 1000)
path = out_dir / f"mici_{panel_name}_{shots_taken + 1:02d}_{ts}.png"
rl.export_image(img, str(path))
rl.unload_image(img)
saved.append(path)
print(f" 📸 saved: {path}")
shots_taken += 1
next_shot_frame += int(args.fps * 0.5) # 0.5s between shots
rl.unload_render_texture(render_tex)
rl.close_window()
return saved
# ─────────────────────────────────────────────────────────────────────────────
# Video mode
# ─────────────────────────────────────────────────────────────────────────────
def run_video(panel_name: str, args, out_dir: Path) -> Path:
"""
Render args.duration seconds at args.fps, pipe raw RGBA frames to ffmpeg → MP4.
Returns the output MP4 path.
"""
from openpilot.system.ui.lib.application import gui_app
out_dir.mkdir(parents=True, exist_ok=True)
ts = int(time.time())
mp4_path = out_dir / f"mici_{panel_name}_{ts}.mp4"
# Launch ffmpeg to accept raw RGBA frames at MICI native resolution
ffmpeg = subprocess.Popen([
"ffmpeg", "-v", "warning", "-nostats",
"-f", "rawvideo", "-pix_fmt", "rgba",
"-s", f"{MICI_W}x{MICI_H}",
"-r", str(args.fps),
"-i", "pipe:0",
"-vf", "vflip,format=yuv420p",
"-c:v", "libx264", "-preset", "fast", "-crf", "18",
"-y", str(mp4_path),
], stdin=subprocess.PIPE)
# Write-queue so rendering doesn't block on ffmpeg
frame_queue: queue.Queue = queue.Queue(maxsize=args.fps * 2)
stop_event = threading.Event()
def _writer():
while not stop_event.is_set() or not frame_queue.empty():
try:
data = frame_queue.get(timeout=0.1)
ffmpeg.stdin.write(data)
except queue.Empty:
pass
ffmpeg.stdin.close()
writer_thread = threading.Thread(target=_writer, daemon=True)
writer_thread.start()
gui_app.init_window(f"MICI Preview (recording) — {panel_name}", fps=args.fps)
render_tex = rl.load_render_texture(MICI_W, MICI_H)
widget = load_panel(panel_name)
win_w = rl.get_screen_width()
win_h = rl.get_screen_height()
total_frames = int(args.fps * args.duration)
frame = 0
settle = min(args.frames, total_frames // 4)
print(f" 🎬 recording {args.duration}s at {args.fps}fps → {mp4_path.name}")
while not rl.window_should_close() and frame < total_frames:
rl.begin_drawing()
rl.clear_background(CHROME_BG)
rl.begin_texture_mode(render_tex)
rl.clear_background(rl.BLACK)
widget.render(rl.Rectangle(0, 0, MICI_W, MICI_H))
rl.end_texture_mode()
src = rl.Rectangle(0, 0, MICI_W, -MICI_H)
dst = rl.Rectangle(0, 0, win_w, win_h)
rl.draw_texture_pro(render_tex.texture, src, dst, rl.Vector2(0, 0), 0, rl.WHITE)
# REC progress bar
pct = frame / total_frames
rl.draw_rectangle(0, win_h - 3, int(win_w * pct), 3, rl.Color(255, 80, 80, 180))
rl.draw_text(f"REC {frame}/{total_frames}", 6, 6, 11, rl.Color(255, 80, 80, 200))
rl.end_drawing()
# Queue raw RGBA from MICI-res texture for ffmpeg
if frame >= settle:
import ctypes
img = rl.load_image_from_texture(render_tex.texture)
colors = rl.load_image_colors(img)
raw = bytes(ctypes.string_at(colors, MICI_W * MICI_H * 4))
rl.unload_image_colors(colors)
rl.unload_image(img)
try:
frame_queue.put(raw, timeout=1.0)
except queue.Full:
pass
frame += 1
rl.unload_render_texture(render_tex)
rl.close_window()
stop_event.set()
writer_thread.join(timeout=10)
ffmpeg.wait(timeout=15)
print(f" ✅ video saved: {mp4_path}")
return mp4_path
# ─────────────────────────────────────────────────────────────────────────────
# Live / hot-reload mode
# ─────────────────────────────────────────────────────────────────────────────
def run_live(panel_name: str, args, mock_params):
"""
Interactive window with hot-reload.
Watches the source file of the selected panel; re-imports it on save.
Press S to take a screenshot, R to force reload, Q/Esc to quit.
"""
from openpilot.system.ui.lib.application import gui_app
gui_app.init_window(f"MICI Live — {panel_name} [S=shot R=reload Q=quit]", fps=args.fps)
render_tex = rl.load_render_texture(MICI_W, MICI_H)
win_w = rl.get_screen_width()
win_h = rl.get_screen_height()
widget = load_panel(panel_name)
last_mtime: dict[str, float] = {}
def _watch_paths() -> list[Path]:
"""Files to watch for changes (panel module + shared theme/button)."""
mods = [
ROOT / "selfdrive/ui/iqpilot/mici/layouts" / f"{panel_name}.py",
ROOT / "selfdrive/ui/iqpilot/theme.py",
ROOT / "selfdrive/ui/mici/widgets/button.py",
]
return [p for p in mods if p.exists()]
def _needs_reload() -> bool:
for p in _watch_paths():
mtime = p.stat().st_mtime
if last_mtime.get(str(p), 0) != mtime:
last_mtime[str(p)] = mtime
return True
return False
def _reload():
nonlocal widget
print(" 🔄 reloading...")
# Invalidate cached modules so importlib picks up changes
prefix = "openpilot.iqpilot.ui.mici.layouts"
for key in list(sys.modules.keys()):
if key.startswith(prefix) or key == "openpilot.iqpilot.ui.theme":
del sys.modules[key]
try:
widget = load_panel(panel_name)
print(" ✅ reloaded OK")
except Exception as e:
print(f" ❌ reload error: {e}")
# Seed mtimes
for p in _watch_paths():
last_mtime[str(p)] = p.stat().st_mtime
out_dir = Path(args.out)
shot_count = 0
while not rl.window_should_close():
# Check for file changes
if _needs_reload():
_reload()
# Key bindings
if rl.is_key_pressed(rl.KeyboardKey.KEY_Q) or rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
break
if rl.is_key_pressed(rl.KeyboardKey.KEY_R):
_reload()
if rl.is_key_pressed(rl.KeyboardKey.KEY_S):
# Take screenshot from render texture (native MICI res)
img = rl.load_image_from_texture(render_tex.texture)
rl.image_flip_vertical(img)
rl.image_resize(img, MICI_W * 2, MICI_H * 2)
out_dir.mkdir(parents=True, exist_ok=True)
ts = int(time.time() * 1000)
path = out_dir / f"mici_{panel_name}_live_{shot_count:03d}_{ts}.png"
rl.export_image(img, str(path))
rl.unload_image(img)
shot_count += 1
print(f" 📸 screenshot saved: {path}")
if args.open:
subprocess.Popen(["open", str(path)])
rl.begin_drawing()
rl.clear_background(CHROME_BG)
# Render widget into MICI-sized texture, blit to full window
rl.begin_texture_mode(render_tex)
rl.clear_background(rl.BLACK)
widget.render(rl.Rectangle(0, 0, MICI_W, MICI_H))
rl.end_texture_mode()
src = rl.Rectangle(0, 0, MICI_W, -MICI_H)
dst = rl.Rectangle(0, 0, win_w, win_h)
rl.draw_texture_pro(render_tex.texture, src, dst, rl.Vector2(0, 0), 0, rl.WHITE)
# Status hint overlay
watch_files = _watch_paths()
hint = f"watching {len(watch_files)} file(s) | S=shot R=reload Q=quit"
rl.draw_text(hint, 6, win_h - 18, 11, rl.Color(80, 80, 80, 160))
rl.end_drawing()
rl.unload_render_texture(render_tex)
rl.close_window()
# ─────────────────────────────────────────────────────────────────────────────
# Entry point
# ─────────────────────────────────────────────────────────────────────────────
def parse_args():
p = argparse.ArgumentParser(
description="IQ.Pilot MICI UI Preview Tool",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
p.add_argument("--panel", default="steering",
help="Panel name or 'all'")
p.add_argument("--mode", default="screenshot",
choices=["screenshot", "video", "live"],
help="Capture mode")
p.add_argument("--frames", type=int, default=90,
help="Settle frames before screenshot")
p.add_argument("--shots", type=int, default=1,
help="Number of screenshots")
p.add_argument("--duration", type=float, default=4.0,
help="Video duration in seconds")
p.add_argument("--fps", type=int, default=60,
help="Render FPS")
p.add_argument("--scale", type=float, default=2.5,
help="Window scale multiplier (1.0 = native 536×240)")
p.add_argument("--out", default=str(DEFAULT_OUT),
help="Output directory")
p.add_argument("--open", action="store_true", default=True,
help="Open output after capture (macOS)")
p.add_argument("--no-open", dest="open", action="store_false")
p.add_argument("--mock", action="store_true", default=True,
help="Use mock UI state (no live process needed)")
p.add_argument("--no-mock", dest="mock", action="store_false")
p.add_argument("--accent", default=None,
help="Override neon accent color, e.g. '#FF6B00'")
return p.parse_args()
def main():
args = parse_args()
out_dir = Path(args.out)
out_dir.mkdir(parents=True, exist_ok=True)
# Set SCALE env var BEFORE gui_app module is imported (it reads it at import time)
os.environ["SCALE"] = str(args.scale)
# Apply mock state early (before any widget imports)
mock_params = None
if args.mock:
print(" 🎭 using mock UI state (--no-mock to use live params)")
mock_params = _patch_mock_state()
# Override accent color if requested
if args.accent:
if mock_params:
mock_params.put("UIAccentColor", args.accent)
print(f" 🎨 accent color: {args.accent}")
panels = ALL_PANELS if args.panel == "all" else [args.panel]
mode = args.mode
if mode == "live" and len(panels) > 1:
print(" ⚠️ live mode only supports a single panel. Using first panel.")
panels = panels[:1]
all_outputs: list[Path] = []
for panel_name in panels:
print(f"\n{mode.upper()} — panel: {panel_name}")
try:
if mode == "live":
run_live(panel_name, args, mock_params)
elif mode == "screenshot":
saved = run_screenshot(panel_name, args, out_dir)
all_outputs.extend(saved)
elif mode == "video":
mp4 = run_video(panel_name, args, out_dir)
all_outputs.append(mp4)
except Exception as e:
print(f" ❌ failed for panel '{panel_name}': {e}")
import traceback; traceback.print_exc()
if rl.is_window_ready():
rl.close_window()
# Open all outputs at once
if args.open and all_outputs:
print(f"\n 📂 opening {len(all_outputs)} output(s)...")
subprocess.Popen(["open"] + [str(p) for p in all_outputs])
print("\n ✨ done\n")
if __name__ == "__main__":
main()