IQ.Pilot Release Commit @ ba009d5

This commit is contained in:
IQ.Lvbs CI [bot]
2026-09-04 12:33:30 -05:00
parent 2e7a1d1fa5
commit fde770bf03
84 changed files with 4206 additions and 302 deletions

View File

@@ -369,6 +369,11 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
// never meant to survive a manager restart or reach a log.
{"IQAndroidNavEmail", {CLEAR_ON_MANAGER_START | DONT_LOG, STRING}},
{"IQAndroidNavPassword", {CLEAR_ON_MANAGER_START | DONT_LOG, STRING}},
// Report-request flags: the UI writes 1 to ask navassist to file that report in the nav
// app; navassist sets it back to 0 once the app confirms it was sent, so 0 == done.
{"IQNavReportPolice", {PERSISTENT, BOOL, "0"}},
{"IQNavReportCrash", {PERSISTENT, BOOL, "0"}},
{"IQNavReportHazard", {PERSISTENT, BOOL, "0"}},
{"IQLiveSteerDelay", {PERSISTENT, BOOL, "1"}},
{"IQLateralAccelSlew", {PERSISTENT, BOOL, "0"}},
{"IQLateralCurvatureLookahead", {PERSISTENT, BOOL, "0"}},

View File

@@ -513,6 +513,9 @@ class InferenceDaemon:
self._lat_smooth_extra_sec = 0.0
def _load_car_params(self, demo: bool):
if not demo and self._params.get_bool("IQBenchIgnition") and self._params.get("CarParams") is None:
cloudlog.warning("iqmodeld: bench ignition with no CarParams; running with the demo car")
demo = True
car_params = get_demo_car_params() if demo else messaging.log_from_bytes(
self._params.get("CarParams", block=True), car.CarParams)
cloudlog.info("iqmodeld got CarParams: %s", car_params.brand)

View File

@@ -87,8 +87,6 @@ def main() -> None:
print(f"saved warp JIT to {out} ({os.path.getsize(out) / 1e6:.2f} MB)")
if __name__ == "__main__":
main()
def selftest_inputs(cam_w: int, cam_h: int, nv12_size: int):
@@ -117,3 +115,7 @@ def selftest_digest(compiled, cam_w: int, cam_h: int, nv12_size: int) -> str:
frame=Tensor(frame, device=dev).realize(),
big_frame=Tensor(big_frame, device=dev).realize())
return hashlib.sha256(out.numpy().astype(np.uint8).tobytes()).hexdigest()
if __name__ == "__main__":
main()

View File

@@ -552,7 +552,8 @@ class ModelsLayoutMici(NavScroller):
def _update_steer_delay_subtext(self):
if self._steer_delay._checked:
try:
self._steer_delay.set_value(f"measured {ui_state.sm['lateralDelay'].lateralDelay:.3f} s")
measured = ui_state.measured_steer_delay()
self._steer_delay.set_value("calibrating" if measured is None else f"measured {measured:.3f} s")
except Exception:
self._steer_delay.set_value("")
return

View File

@@ -54,7 +54,9 @@ MICI_BORDER_THICKNESS = 50
MICI_BORDER_ROUNDNESS = 0.2 * 1.02
MICI_BORDER_BOTTOM_ONLY_HEIGHT = 95
MICI_EXPERIMENTAL_ICON_SIZE = 28
MICI_EXPERIMENTAL_ICON_SPACING = 8
MICI_EXPERIMENTAL_ICON_SLOT = 60
MICI_EXPERIMENTAL_ICON_MARGIN_X = 16
MICI_EXPERIMENTAL_ICON_MARGIN_Y = 10
class BookmarkIcon(Widget):
@@ -274,7 +276,7 @@ class AugmentedRoadView(CameraView):
# don't draw the experimental/IQ.Dynamic icon over alert text (it falls back to the
# top-left alert anchor when the DMoji is hidden while disengaged)
if alert_to_render is None:
self._draw_experimental_icon(should_draw_dmoji)
self._draw_experimental_icon()
# End clipping region
rl.end_scissor_mode()
@@ -304,7 +306,7 @@ class AugmentedRoadView(CameraView):
rl.draw_rectangle(int(self.rect.x), int(self.rect.y), int(self.rect.width), int(self.rect.height), rl.Color(0, 0, 0, 175))
self._offroad_label.render(self._content_rect)
def _draw_experimental_icon(self, draw_below_driver_state: bool) -> None:
def _draw_experimental_icon(self) -> None:
if not ui_state.started:
return
@@ -316,13 +318,10 @@ class AugmentedRoadView(CameraView):
else:
icon = self._iqstandard_txt
if draw_below_driver_state:
pos_x = self._rect.x + 16 + (self._driver_state_renderer.rect.width - icon.width) / 2
pos_y = self._rect.y + 10 + self._driver_state_renderer.rect.height + MICI_EXPERIMENTAL_ICON_SPACING
else:
pos_x = self._rect.x + 18
pos_y = self._rect.y + 18
slot_x = self._content_rect.x + self._content_rect.width - MICI_EXPERIMENTAL_ICON_MARGIN_X - MICI_EXPERIMENTAL_ICON_SLOT
slot_y = self._content_rect.y + MICI_EXPERIMENTAL_ICON_MARGIN_Y
pos_x = slot_x + (MICI_EXPERIMENTAL_ICON_SLOT - icon.width) / 2
pos_y = slot_y + (MICI_EXPERIMENTAL_ICON_SLOT - icon.height) / 2
rl.draw_texture(icon, int(pos_x), int(pos_y), rl.WHITE)
def _draw_border(self):

View File

@@ -14,6 +14,7 @@ from iqpilot.selfdrive.ui.mici.onroad import blend_colors
from iqpilot.system.ui.lib.application import gui_app
from iqpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
from iqpilot.system.ui.widgets import Widget
from iqpilot.ui.theme import NeonTheme
CLIP_MARGIN = 500
MIN_DRAW_DISTANCE = 10.0
@@ -297,7 +298,10 @@ class ModelRenderer(Widget):
def _get_ll_color(self, prob: float, adjacent: bool, left: bool):
alpha = np.clip(prob, 0.0, 0.7)
if adjacent:
_base_color = LANE_LINE_COLORS.get(ui_state.status, LANE_LINE_COLORS[UIStatus.DISENGAGED])
if gui_app.iqpilot_ui() and ui_state.status == UIStatus.ENGAGED:
_base_color = NeonTheme.glow()
else:
_base_color = LANE_LINE_COLORS.get(ui_state.status, LANE_LINE_COLORS[UIStatus.DISENGAGED])
color = rl.Color(_base_color.r, _base_color.g, _base_color.b, int(alpha * 255))
# turn adjacent lls orange if torque is high

View File

@@ -424,6 +424,13 @@ class UIState(IQUIState):
self._started_prev = self.started
def measured_steer_delay(self) -> float | None:
# estimatord is onroad-only, so offroad the lateralDelay socket reads back a default 0.0
if self.sm.alive['lateralDelay']:
return float(self.sm['lateralDelay'].lateralDelay)
lag = log_param_from_bytes(self.params, "LiveDelay", log.Event)
return float(lag.lateralDelay.lateralDelay) if lag is not None else None
def update_params(self) -> None:
CP = log_param_from_bytes(self.params, "CarParamsPersistent", car.CarParams)
if CP is not None:

View File

@@ -1,200 +0,0 @@
# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
from __future__ import annotations
import json
import os
import socket
import subprocess
import time
from iqpilot.cereal import messaging
from iqpilot.common.params import Params
from iqpilot.common.realtime import Ratekeeper
from iqpilot.common.swaglog import cloudlog
ANDROID_ROOT = "/data/android"
HEADLESS = f"{ANDROID_ROOT}/waydroid_headless.sh"
TRIM = f"{ANDROID_ROOT}/android_trim.sh"
GUARD = f"{ANDROID_ROOT}/android_guard.sh"
LXC_ATTACH = f"{ANDROID_ROOT}/root/usr/bin/lxc-attach"
LD = f"{ANDROID_ROOT}/root/usr/lib/aarch64-linux-gnu"
# Waze's launcher is FreeMapAppActivity; there is no com.waze.MainActivity, and asking for
# one fails with "Activity class does not exist" rather than anything that names the problem.
NAV_APPS = {
"waze": ("com.waze", "com.waze/.FreeMapAppActivity"),
"maps": ("com.google.android.apps.maps", "com.google.android.apps.maps/com.google.android.maps.MapsActivity"),
}
BRIDGE_HOST = "192.168.240.112"
BRIDGE_PORT = 8099
GPS_SOURCES = ["iqLiveLocation", "liveLocationKalman", "gpsLocationExternal", "gpsLocation"]
PROVIDERS = ("gps", "fused", "network")
GPS_INTERVAL_S = 0.5
def attach(*args: str, timeout: float = 10.0) -> subprocess.CompletedProcess:
cmd = ["sudo", "-n", "env", f"LD_LIBRARY_PATH={LD}", LXC_ATTACH,
"-P", "/var/lib/waydroid/lxc", "-n", "waydroid", "--", *args]
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=False)
def container_booted() -> bool:
try:
return attach("/system/bin/getprop", "sys.boot_completed", timeout=8).stdout.strip() == "1"
except (subprocess.SubprocessError, OSError):
return False
def bring_up() -> None:
# AGNOS mounts / read-only, so no unit file can be installed; a transient unit is the only
# way the compositor and lxc-start survive this process exiting.
subprocess.run(["sudo", "-n", "systemd-run", "--unit=iq-android", "--service-type=oneshot",
"--remain-after-exit", HEADLESS, "up"],
capture_output=True, text=True, timeout=120, check=False)
def start_guard() -> None:
subprocess.run(["sudo", "-n", "systemd-run", "--unit=iq-android-guard", GUARD],
capture_output=True, text=True, timeout=30, check=False)
def trim(nav_app: str) -> None:
subprocess.run(["sudo", "-n", "env", f"ANDROID_NAV_APP={nav_app}", TRIM],
capture_output=True, timeout=240, check=False)
def enable_mock_location() -> None:
# Wiped by every container restart, and the package form does not cover uid 0: without the
# --uid form every injection fails with SecurityException and the apps just say "no GPS".
attach("/system/bin/cmd", "appops", "set", "--uid", "0", "android:mock_location", "allow")
for p in PROVIDERS:
attach("/system/bin/cmd", "location", "providers", "add-test-provider", p)
attach("/system/bin/cmd", "location", "providers", "set-test-provider-enabled", p, "true")
# The a11y tree is only updated while the display is awake; asleep it emits nothing at all.
attach("/system/bin/svc", "power", "stayon", "true")
def inject(lat: float, lon: float) -> None:
for p in PROVIDERS:
attach("/system/bin/cmd", "location", "providers", "set-test-provider-location", p,
"--location", f"{lat:.6f},{lon:.6f}", "--accuracy", "4", timeout=6)
def read_position(sm: messaging.SubMaster) -> tuple[float, float] | None:
for src in GPS_SOURCES:
if src not in sm.data or not sm.valid.get(src, False):
continue
msg = sm[src]
for lat_a, lon_a in (("latitude", "longitude"), ("lat", "lon")):
lat = getattr(msg, lat_a, None)
lon = getattr(msg, lon_a, None)
if lat is not None and lon is not None and (lat or lon):
return float(lat), float(lon)
return None
class AndroidNavDaemon:
def __init__(self) -> None:
self.params = Params()
self.sources = [s for s in GPS_SOURCES if s in messaging.SERVICE_LIST]
self.sm = messaging.SubMaster(self.sources) if self.sources else None
self.last_inject = 0.0
self.up = False
def status(self, text: str) -> None:
self.params.put("IQAndroidNavStatus", text)
def sign_in(self) -> None:
email = self.params.get("IQAndroidNavEmail", encoding="utf8")
password = self.params.get("IQAndroidNavPassword", encoding="utf8")
if not email or not password:
return
try:
with socket.create_connection((BRIDGE_HOST, BRIDGE_PORT), timeout=10) as s:
s.sendall(json.dumps({"cmd": "signin", "email": email, "password": password}).encode() + b"\n")
self.status("sign-in requested")
except OSError:
cloudlog.exception("androidd: sign-in request failed")
self.status("sign-in failed")
finally:
# Never let the credential outlive the one use it was handed over for.
self.params.remove("IQAndroidNavEmail")
self.params.remove("IQAndroidNavPassword")
def nav_app(self) -> str:
value = self.params.get("IQAndroidNavApp", encoding="utf8") or "waze"
return value if value in NAV_APPS else "waze"
def enforce_single_app(self, nav_app: str) -> None:
for name, (pkg, _) in NAV_APPS.items():
if name != nav_app:
attach("/system/bin/am", "force-stop", pkg)
def ensure_app_running(self, nav_app: str) -> None:
pkg, component = NAV_APPS[nav_app]
if attach("/system/bin/pidof", pkg, timeout=8).stdout.strip():
return
attach("/system/bin/am", "start", "-n", component, timeout=20)
def ensure_up(self) -> None:
if container_booted():
if not self.up:
nav_app = self.nav_app()
enable_mock_location()
trim(nav_app)
self.enforce_single_app(nav_app)
start_guard()
self.status(f"running:{nav_app}")
self.up = True
return
self.up = False
self.status("starting")
bring_up()
def step(self) -> None:
if not self.params.get_bool("IQAndroidNav"):
if self.up:
subprocess.run(["sudo", "-n", "systemctl", "stop", "iq-android", "iq-android-guard"],
capture_output=True, timeout=60, check=False)
self.up = False
self.status("disabled")
return
self.ensure_up()
if not self.up:
return
self.sign_in()
self.ensure_app_running(self.nav_app())
if self.sm is None:
return
self.sm.update(0)
now = time.monotonic()
if now - self.last_inject < GPS_INTERVAL_S:
return
position = read_position(self.sm)
if position is not None:
self.last_inject = now
try:
inject(*position)
except subprocess.SubprocessError:
cloudlog.exception("androidd: location inject failed")
def main() -> None:
if not os.path.exists(HEADLESS):
cloudlog.warning("androidd: android stack not staged, exiting")
return
daemon = AndroidNavDaemon()
rk = Ratekeeper(2.0, print_delay_threshold=None)
while True:
try:
daemon.step()
except Exception:
cloudlog.exception("androidd: step failed")
rk.keep_time()
if __name__ == "__main__":
main()

View File

@@ -427,8 +427,6 @@ def hardware_thread(end_event, hw_queue) -> None:
# Set ignition based on any panda connected
onroad_conditions["ignition"] = any(ps.ignitionLine or ps.ignitionCan for ps in pandaStates if ps.pandaType != log.PandaState.PandaType.unknown)
if params.get_bool("IQBenchIgnition"):
onroad_conditions["ignition"] = True
pandaState = pandaStates[0]
@@ -439,6 +437,9 @@ def hardware_thread(end_event, hw_queue) -> None:
onroad_conditions["ignition"] = False
cloudlog.error("panda timed out onroad")
if params.get_bool("IQBenchIgnition"):
onroad_conditions["ignition"] = True
# Run at 2Hz, plus either edge of ignition
ign_edge = (started_ts is not None) != all(onroad_conditions.values())
if (sm.frame % round(SERVICE_LIST['pandaStates'].frequency * DT_HW) != 0) and not ign_edge:
@@ -470,7 +471,7 @@ def hardware_thread(end_event, hw_queue) -> None:
online_cpu_usage = [int(round(n)) for n in psutil.cpu_percent(percpu=True)]
offline_cpu_usage = [0., ] * (len(msg.deviceState.cpuTempC) - len(online_cpu_usage))
msg.deviceState.cpuUsagePercent = online_cpu_usage + offline_cpu_usage
if msg.deviceState.memoryUsagePercent > 85:
if msg.deviceState.memoryUsagePercent > 95:
avg_cpu_usage = int(round(sum(online_cpu_usage) / max(1, len(online_cpu_usage))))
perf.emit(
"hardware_low_memory",

View File

@@ -67,7 +67,7 @@ def qcomgps(started: bool, params: Params, CP: car.CarParams) -> bool:
def always_run(started: bool, params: Params, CP: car.CarParams) -> bool:
return True
def android_nav(started: bool, params: Params, CP: car.CarParams) -> bool:
def nav_assist(started: bool, params: Params, CP: car.CarParams) -> bool:
return params.get_bool("IQAndroidNav")
def only_onroad(started: bool, params: Params, CP: car.CarParams) -> bool:
@@ -179,7 +179,7 @@ procs = [
PythonProcess("journald", "iqpilot.system.journald", only_onroad, platform.system() != "Darwin"),
PythonProcess("micd", "iqpilot.system.micd", or_(iscar, livestream)),
PythonProcess("timed", "iqpilot.system.timed", always_run, enabled=not PC),
PythonProcess("androidd", "iqpilot.system.android.androidd", android_nav, enabled=TICI, restart_if_crash=True),
BundleProcess("navassistd", "iqpilot_navassist_private", "iqpilot_private.navassist.daemon", nav_assist, enabled=TICI, restart_if_crash=True),
PythonProcess("dmonitoringmodeld", "iqpilot.selfdrive.dmonitoringmodeld.dmonitoringmodeld", driver_monitoring, enabled=not PC),

View File

@@ -900,7 +900,11 @@ class SteeringLayout(Widget):
delay_desc = tr("Let IQ.Pilot measure how long your steering takes to respond and keep that figure up to date. "
"Switch it off to pin the timing yourself.")
if live_delay:
delay_desc += f"<br>{tr('Measured:')} {ui_state.sm['lateralDelay'].lateralDelay:.3f} s"
measured = ui_state.measured_steer_delay()
if measured is None:
delay_desc += f"<br>{tr('Measured:')} {tr('not yet, drive to calibrate')}"
else:
delay_desc += f"<br>{tr('Measured:')} {measured:.3f} s"
elif ui_state.CP:
sw = float(ui_state.params.get("IQSoftwareSteerDelay", "0.2"))
cp = ui_state.CP.steerActuatorDelay

View File

@@ -9,10 +9,10 @@ from iqpilot.system.ui.iqwidgets.lib import canvas
from iqpilot.system.ui.lib.application import gui_app
from iqpilot.ui.onroad.hud_overlays import IQSpeedLimitOverlay, _SL_ASSIST, _SL_DARK, _dim
_SIGN_X = 16
_SIGN_Y = 108
_SIGN_W = 60
_SIGN_H = 64
_SIGN_X = 17
_SIGN_Y = 86
_SIGN_W = 58
_SIGN_H = 74
_BADGE_SIDE = 24
@@ -47,8 +47,8 @@ class MiciSpeedLimitSign(IQSpeedLimitOverlay):
(self._vienna if ui_state.is_metric else self._mutcd)(box, value, badge, tint, has_limit, alpha)
def _vienna(self, rect, value, badge, tint, has_limit, alpha=1.0):
hub = canvas.Pt(rect.x + rect.width / 2, rect.y + rect.height / 2)
radius = rect.width / 2
hub = canvas.Pt(rect.x + radius, rect.y + rect.height / 2)
canvas.disc_at(hub, radius, _dim(canvas.WHITE, alpha))
canvas.annulus(hub, radius * 0.78, radius, 0, 360, 36, _dim(canvas.RED, alpha))
canvas.glyphs_centered(self._bold, value, 22 if len(value) >= 3 else 28, hub, _dim(tint, alpha))
@@ -64,9 +64,9 @@ class MiciSpeedLimitSign(IQSpeedLimitOverlay):
inner = canvas.Box(rect.x + 4, rect.y + 4, rect.width - 8, rect.height - 8)
canvas.panel_outline(inner, 0.25, 8, 2, _dim(canvas.BLACK, alpha))
mid = rect.x + rect.width / 2
canvas.glyphs_centered(self._demi, "SPEED", 12, canvas.Pt(mid, rect.y + 14), _dim(canvas.BLACK, alpha))
canvas.glyphs_centered(self._demi, "LIMIT", 12, canvas.Pt(mid, rect.y + 25), _dim(canvas.BLACK, alpha))
canvas.glyphs_centered(self._bold, value, 30 if len(value) <= 2 else 24, canvas.Pt(mid, rect.y + 45), _dim(tint, alpha))
canvas.glyphs_centered(self._demi, "SPEED", 13, canvas.Pt(mid, rect.y + 16), _dim(canvas.BLACK, alpha))
canvas.glyphs_centered(self._demi, "LIMIT", 13, canvas.Pt(mid, rect.y + 29), _dim(canvas.BLACK, alpha))
canvas.glyphs_centered(self._bold, value, 34 if len(value) <= 2 else 27, canvas.Pt(mid, rect.y + 52), _dim(tint, alpha))
if badge:
chip = canvas.Box(rect.x + rect.width - _BADGE_SIDE * 0.55, rect.y - _BADGE_SIDE * 0.55, _BADGE_SIDE, _BADGE_SIDE)
canvas.panel(chip, 0.35, 8, _dim(canvas.BLACK, alpha))