IQ.Pilot Release Commit @ 27f668a
This commit is contained in:
@@ -238,6 +238,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"IQEmacEnabled", {PERSISTENT, BOOL, "0"}},
|
||||
{"IQEmacHost", {PERSISTENT, STRING}},
|
||||
{"IQEmacModel", {PERSISTENT, STRING}},
|
||||
{"IQEmacSmallModel", {PERSISTENT, BOOL, "0"}},
|
||||
{"IQEmacCatalogCache", {PERSISTENT, STRING}},
|
||||
{"MacModelDownloadProgress", {CLEAR_ON_MANAGER_START, STRING, "1.0"}},
|
||||
{"MacModelStatus", {CLEAR_ON_MANAGER_START, STRING}},
|
||||
@@ -315,6 +316,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"IQEmacEnabled", {PERSISTENT, BOOL, "0"}},
|
||||
{"IQEmacHost", {PERSISTENT, STRING}},
|
||||
{"IQEmacModel", {PERSISTENT, STRING}},
|
||||
{"IQEmacSmallModel", {PERSISTENT, BOOL, "0"}},
|
||||
{"IQEmacCatalogCache", {PERSISTENT, JSON}},
|
||||
{"IQEgpuDisabled", {PERSISTENT, BOOL, "0"}},
|
||||
{"IQEgpuEnabled", {PERSISTENT, BOOL, "0"}},
|
||||
@@ -359,6 +361,14 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
|
||||
// iqpilot model params
|
||||
{"CameraOffset", {PERSISTENT, FLOAT, "0.0"}},
|
||||
{"IQAndroidNav", {PERSISTENT, BOOL, "0"}},
|
||||
{"IQAndroidNavStatus", {PERSISTENT, STRING}},
|
||||
// "waze" or "maps": exactly one runs, the other is disabled outright.
|
||||
{"IQAndroidNavApp", {PERSISTENT, STRING, "waze"}},
|
||||
// Consumed by androidd to sign the container in once, then cleared: the credential is
|
||||
// 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}},
|
||||
{"IQLiveSteerDelay", {PERSISTENT, BOOL, "1"}},
|
||||
{"IQLateralAccelSlew", {PERSISTENT, BOOL, "0"}},
|
||||
{"IQLateralCurvatureLookahead", {PERSISTENT, BOOL, "0"}},
|
||||
|
||||
@@ -235,6 +235,7 @@ class Controls(IQControlsLayer):
|
||||
|
||||
actuators = CC.actuators
|
||||
actuators.longControlState = self.LoC.long_control_state
|
||||
actuators.speed = float(max(long_plan.speeds, default=0.0))
|
||||
|
||||
if not CC.latActive:
|
||||
self.LaC.reset()
|
||||
|
||||
@@ -63,6 +63,11 @@ def resolve_model_name(params, keys) -> str:
|
||||
from iqpilot.selfdrive.iqmodeld.egpu_model import DEFAULT_EGPU_MODEL, resolve_egpu_model
|
||||
resolved = resolve_egpu_model(params, allow_refresh=False)
|
||||
return resolved["key"] if resolved else DEFAULT_EGPU_MODEL
|
||||
if params.get_bool("IQEmacSmallModel"):
|
||||
from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle
|
||||
bundle = get_active_bundle(params)
|
||||
if bundle is not None and (bundle.internalName or bundle.displayName):
|
||||
return bundle.internalName or bundle.displayName
|
||||
name = params.get("IQEmacModel") or b"lebrowski"
|
||||
return name.decode() if isinstance(name, bytes) else name
|
||||
|
||||
@@ -168,7 +173,7 @@ def _patch_and_send(pm: PubMaster, payload: dict, frame_drop_perc: float, select
|
||||
if mismatch is None:
|
||||
mismatch = source_lag > 0
|
||||
|
||||
big = payload.get("source") in BIG_SOURCES
|
||||
big = bool(payload.get("big", payload.get("source") in BIG_SOURCES))
|
||||
model_msg = log_from_bytes(msgs["modelV2"]).as_builder()
|
||||
if mismatch:
|
||||
model_msg.modelV2.frameId = target
|
||||
|
||||
@@ -272,6 +272,23 @@ class TestChannelContract:
|
||||
assert sent["modelV2"].modelV2.frameDropPerc == 0.0
|
||||
assert sent["cameraOdometry"].valid
|
||||
|
||||
def test_selector_big_flag_follows_payload_then_source(self):
|
||||
from iqpilot.selfdrive.iqmodeld.modeld_selector import _patch_and_send
|
||||
|
||||
sent = {}
|
||||
|
||||
class PM:
|
||||
def send(self, service, msg):
|
||||
sent[service] = msg
|
||||
|
||||
payload = make_big_channel_payload(42, True, 0.03, 25.0, self._real_msgs())
|
||||
_patch_and_send(PM(), payload, frame_drop_perc=0.0, selector_dropped=0, target=42, source_lag=0)
|
||||
assert sent["modelV2"].modelV2.big and sent["drivingModelData"].drivingModelData.big
|
||||
|
||||
small_on_mac = {**make_big_channel_payload(43, True, 0.03, 25.0, self._real_msgs()), "source": "mac_big", "big": False}
|
||||
_patch_and_send(PM(), small_on_mac, frame_drop_perc=0.0, selector_dropped=0, target=43, source_lag=0)
|
||||
assert not sent["modelV2"].modelV2.big and not sent["drivingModelData"].drivingModelData.big
|
||||
|
||||
def test_selector_lag_patches_frame_id(self):
|
||||
from iqpilot.selfdrive.iqmodeld.modeld_selector import _patch_and_send
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ LONGITUDINAL_MODE_STOCK = 0
|
||||
LONGITUDINAL_MODE_CHILL = 1
|
||||
LONGITUDINAL_MODE_DYNAMIC = 2
|
||||
LONGITUDINAL_MODE_PILOT = 3
|
||||
IQ_LONGITUDINAL_MODES = (LONGITUDINAL_MODE_CHILL, LONGITUDINAL_MODE_DYNAMIC, LONGITUDINAL_MODE_PILOT)
|
||||
|
||||
PERSONALITY_AGGRESSIVE = log.LongitudinalPersonality.schema.enumerants["aggressive"]
|
||||
PERSONALITY_STANDARD = log.LongitudinalPersonality.schema.enumerants["standard"]
|
||||
@@ -60,6 +61,20 @@ def apply_longitudinal_mode(params, mode: int) -> None:
|
||||
raise ValueError(f"invalid longitudinal mode: {mode}")
|
||||
|
||||
|
||||
def longitudinal_mode_needs_cycle(previous: int, mode: int) -> bool:
|
||||
return (previous == LONGITUDINAL_MODE_STOCK) != (mode == LONGITUDINAL_MODE_STOCK)
|
||||
|
||||
|
||||
def next_longitudinal_mode(current: int, onroad: bool, iq_modes_available: bool) -> int:
|
||||
if onroad:
|
||||
order = list(IQ_LONGITUDINAL_MODES)
|
||||
else:
|
||||
order = [LONGITUDINAL_MODE_STOCK] + (list(IQ_LONGITUDINAL_MODES) if iq_modes_available else [])
|
||||
if current not in order:
|
||||
return current if onroad else order[0]
|
||||
return order[(order.index(current) + 1) % len(order)]
|
||||
|
||||
|
||||
def get_follow_distance_state(params) -> tuple[int | None, bool]:
|
||||
mode = get_longitudinal_mode(params)
|
||||
if mode == LONGITUDINAL_MODE_STOCK:
|
||||
|
||||
@@ -7,6 +7,7 @@ import pytest
|
||||
from iqpilot.selfdrive.longitudinal_settings import (
|
||||
LONGITUDINAL_MODE_CHILL,
|
||||
LONGITUDINAL_MODE_DYNAMIC,
|
||||
LONGITUDINAL_MODE_PILOT,
|
||||
LONGITUDINAL_MODE_STOCK,
|
||||
PERSONALITY_AGGRESSIVE,
|
||||
PERSONALITY_RELAXED,
|
||||
@@ -16,6 +17,8 @@ from iqpilot.selfdrive.longitudinal_settings import (
|
||||
get_follow_distance_state,
|
||||
get_longitudinal_mode,
|
||||
get_runtime_personality,
|
||||
longitudinal_mode_needs_cycle,
|
||||
next_longitudinal_mode,
|
||||
set_valid_personality,
|
||||
)
|
||||
|
||||
@@ -108,3 +111,31 @@ def test_dynamic_and_pilot_enable_valid_personality_selection():
|
||||
params.values["IQDynamicMode"] = False
|
||||
|
||||
assert get_follow_distance_state(params) == (PERSONALITY_STANDARD, True)
|
||||
|
||||
|
||||
def test_onroad_cycles_only_between_iq_modes():
|
||||
assert next_longitudinal_mode(LONGITUDINAL_MODE_CHILL, True, True) == LONGITUDINAL_MODE_DYNAMIC
|
||||
assert next_longitudinal_mode(LONGITUDINAL_MODE_DYNAMIC, True, True) == LONGITUDINAL_MODE_PILOT
|
||||
assert next_longitudinal_mode(LONGITUDINAL_MODE_PILOT, True, True) == LONGITUDINAL_MODE_CHILL
|
||||
|
||||
|
||||
def test_onroad_stock_acc_is_locked():
|
||||
assert next_longitudinal_mode(LONGITUDINAL_MODE_STOCK, True, True) == LONGITUDINAL_MODE_STOCK
|
||||
assert next_longitudinal_mode(LONGITUDINAL_MODE_STOCK, True, False) == LONGITUDINAL_MODE_STOCK
|
||||
|
||||
|
||||
def test_offroad_cycles_through_stock_acc():
|
||||
assert next_longitudinal_mode(LONGITUDINAL_MODE_PILOT, False, True) == LONGITUDINAL_MODE_STOCK
|
||||
assert next_longitudinal_mode(LONGITUDINAL_MODE_STOCK, False, True) == LONGITUDINAL_MODE_CHILL
|
||||
|
||||
|
||||
def test_offroad_without_iq_modes_only_offers_stock_acc():
|
||||
assert next_longitudinal_mode(LONGITUDINAL_MODE_STOCK, False, False) == LONGITUDINAL_MODE_STOCK
|
||||
assert next_longitudinal_mode(LONGITUDINAL_MODE_PILOT, False, False) == LONGITUDINAL_MODE_STOCK
|
||||
|
||||
|
||||
def test_cycle_only_when_crossing_stock_boundary():
|
||||
assert longitudinal_mode_needs_cycle(LONGITUDINAL_MODE_STOCK, LONGITUDINAL_MODE_CHILL)
|
||||
assert longitudinal_mode_needs_cycle(LONGITUDINAL_MODE_PILOT, LONGITUDINAL_MODE_STOCK)
|
||||
assert not longitudinal_mode_needs_cycle(LONGITUDINAL_MODE_CHILL, LONGITUDINAL_MODE_PILOT)
|
||||
assert not longitudinal_mode_needs_cycle(LONGITUDINAL_MODE_DYNAMIC, LONGITUDINAL_MODE_CHILL)
|
||||
|
||||
@@ -6,13 +6,17 @@ from iqpilot.common.params import Params, UnknownKeyName
|
||||
from iqpilot.selfdrive.longitudinal_settings import (
|
||||
LONGITUDINAL_MODE_DYNAMIC,
|
||||
LONGITUDINAL_MODE_PILOT,
|
||||
LONGITUDINAL_MODE_STOCK,
|
||||
PERSONALITY_VALUES,
|
||||
apply_longitudinal_mode,
|
||||
get_follow_distance_state,
|
||||
get_longitudinal_mode,
|
||||
longitudinal_mode_needs_cycle,
|
||||
next_longitudinal_mode,
|
||||
set_valid_personality,
|
||||
)
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigButton, BigMultiToggle, BigToggle, BigParamControl
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
|
||||
|
||||
@@ -138,24 +142,48 @@ class IQModeSelector(BigMultiToggle):
|
||||
super().__init__(tr("IQ Mode"), self._display_options)
|
||||
self._params = Params()
|
||||
self._mode_callback = mode_callback
|
||||
self._mode = LONGITUDINAL_MODE_STOCK
|
||||
self._iq_modes_available = False
|
||||
self.refresh()
|
||||
self.set_enabled(lambda: self._next() != self._mode)
|
||||
|
||||
def _index(self) -> int:
|
||||
return get_longitudinal_mode(self._params)
|
||||
|
||||
def is_dynamic(self) -> bool:
|
||||
return self._index() == 2
|
||||
return self._mode == LONGITUDINAL_MODE_DYNAMIC
|
||||
|
||||
def _toyota_factory_long_forced(self) -> bool:
|
||||
cp = ui_state.CP
|
||||
return bool(cp is not None and cp.brand == "toyota" and self._params.get_bool("IQToyotaFactoryLong"))
|
||||
|
||||
def _read_iq_modes_available(self) -> bool:
|
||||
cp = ui_state.CP
|
||||
alpha_available = bool(cp is not None and cp.alphaLongitudinalAvailable)
|
||||
return alpha_available or self._params.get_bool("AlphaLongitudinalEnabled") or self._toyota_factory_long_forced()
|
||||
|
||||
def _next(self) -> int:
|
||||
return next_longitudinal_mode(self._mode, ui_state.is_onroad(), self._iq_modes_available)
|
||||
|
||||
def refresh(self):
|
||||
self.set_value(self._display_options[self._index()])
|
||||
self._mode = self._index()
|
||||
self._iq_modes_available = self._read_iq_modes_available()
|
||||
self.set_value(self._display_options[self._mode])
|
||||
|
||||
def _apply(self, idx: int):
|
||||
previous = self._mode
|
||||
toyota_forced = self._toyota_factory_long_forced()
|
||||
apply_longitudinal_mode(self._params, idx)
|
||||
self._params.put_bool("OnroadCycleRequested", True)
|
||||
if idx != LONGITUDINAL_MODE_STOCK and toyota_forced:
|
||||
self._params.put_bool("IQToyotaFactoryLong", False)
|
||||
if longitudinal_mode_needs_cycle(previous, idx) or (idx != LONGITUDINAL_MODE_STOCK and toyota_forced):
|
||||
self._params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos):
|
||||
nxt = (self._index() + 1) % len(self.OPTIONS)
|
||||
nxt = self._next()
|
||||
if nxt == self._mode:
|
||||
return
|
||||
self._apply(nxt)
|
||||
self.set_value(self._display_options[nxt])
|
||||
self.refresh()
|
||||
if self._mode_callback:
|
||||
self._mode_callback()
|
||||
|
||||
@@ -128,6 +128,8 @@ class ModelsLayoutMici(NavScroller):
|
||||
self._big = BigButton(tr("big model"))
|
||||
self._big.set_click_callback(self._show_big_models)
|
||||
|
||||
self._small_on_mac = BigParamControl(tr("active model on eMac"), "IQEmacSmallModel", toggle_callback=self._small_on_mac_toggled)
|
||||
|
||||
self._cancel = BigButton(tr("stop download"))
|
||||
self._cancel.set_click_callback(self._cancel_model_request)
|
||||
self._cancel.set_visible(self._is_downloading)
|
||||
@@ -158,7 +160,8 @@ class ModelsLayoutMici(NavScroller):
|
||||
self._lane_speed = MappedParamToggle(tr("lane turn speed"), "IQLaneTurnValue", [tr("slow"), tr("normal"), tr("fast")], _LANE_TURN_VALUES)
|
||||
self._lane_speed.set_visible(lambda: self._lane_turn._checked)
|
||||
|
||||
self._main_items = [self._current, self._big, self._cancel, self._supercombo, self._vision, self._policy, self._redownload, self._refresh, self._clear,
|
||||
self._main_items = [self._current, self._big, self._small_on_mac, self._cancel, self._supercombo, self._vision, self._policy,
|
||||
self._redownload, self._refresh, self._clear,
|
||||
self._steer_delay, self._sw_delay, self._lane_turn, self._lane_speed]
|
||||
self._scroller.add_widgets(self._main_items)
|
||||
|
||||
@@ -394,9 +397,28 @@ class ModelsLayoutMici(NavScroller):
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
def _small_on_mac_toggled(self, checked: bool) -> None:
|
||||
p = ui_state.params
|
||||
if checked:
|
||||
p.put_bool("IQEmacEnabled", True)
|
||||
else:
|
||||
p.put_bool("IQEmacEnabled", bool(p.get("IQEmacModel")))
|
||||
|
||||
def _small_on_mac_value(self) -> str:
|
||||
try:
|
||||
active = self.model_manager.activeBundle
|
||||
name = _display_model_name(active) if active and active.ref else ""
|
||||
except Exception:
|
||||
name = ""
|
||||
return f"{name} ({tr('eMac')})" if name else tr("active model")
|
||||
|
||||
def _big_model_value(self) -> str:
|
||||
p = ui_state.params
|
||||
dock = bool(getattr(ui_state.sm["deviceState"], "egpuDockPresent", False))
|
||||
if p.get_bool("IQEmacEnabled") and p.get_bool("IQEmacSmallModel"):
|
||||
progress = self._big_setup_progress()
|
||||
value = self._small_on_mac_value()
|
||||
return f"{value} {int(progress * 100)}%" if progress is not None and progress < 1.0 else value
|
||||
if not p.get_bool("IQEmacEnabled") and not dock:
|
||||
return tr("Off")
|
||||
key = p.get("IQEmacModel")
|
||||
@@ -577,5 +599,5 @@ class ModelsLayoutMici(NavScroller):
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
for w in (self._steer_delay, self._sw_delay, self._lane_turn, self._lane_speed):
|
||||
for w in (self._steer_delay, self._sw_delay, self._lane_turn, self._lane_speed, self._small_on_mac):
|
||||
w.refresh()
|
||||
|
||||
0
iqpilot/system/android/__init__.py
Normal file
0
iqpilot/system/android/__init__.py
Normal file
200
iqpilot/system/android/androidd.py
Normal file
200
iqpilot/system/android/androidd.py
Normal file
@@ -0,0 +1,200 @@
|
||||
# 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()
|
||||
@@ -67,6 +67,9 @@ 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:
|
||||
return params.get_bool("IQAndroidNav")
|
||||
|
||||
def only_onroad(started: bool, params: Params, CP: car.CarParams) -> bool:
|
||||
return started
|
||||
|
||||
@@ -176,6 +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),
|
||||
|
||||
PythonProcess("dmonitoringmodeld", "iqpilot.selfdrive.dmonitoringmodeld.dmonitoringmodeld", driver_monitoring, enabled=not PC),
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
venv_site_packages = os.path.join(Dir("#").abspath, ".venv", "lib", "python3.12", "site-packages")
|
||||
if os.path.isdir(venv_site_packages) and venv_site_packages not in sys.path:
|
||||
sys.path.insert(0, venv_site_packages)
|
||||
|
||||
import imgui
|
||||
import iqdbc
|
||||
import libusb
|
||||
|
||||
@@ -49,7 +49,7 @@ void ReplayStream::mergeSegments() {
|
||||
}
|
||||
|
||||
bool ReplayStream::loadRoute(const std::string &route, const std::string &data_dir, uint32_t replay_flags, bool auto_source) {
|
||||
replay.reset(new Replay(route, {"can", "narrowRoadEncodeIdx", "cabinEncodeIdx", "wideRoadEncodeIdx", "carParams"},
|
||||
replay.reset(new Replay(route, {"can", "roadEncodeIdx", "driverEncodeIdx", "wideRoadEncodeIdx", "carParams"},
|
||||
{}, nullptr, replay_flags, data_dir, auto_source));
|
||||
replay->setSegmentCacheLimit(settings.max_cached_minutes);
|
||||
replay->installEventFilter([this](const Event *event) { return eventFilter(event); });
|
||||
|
||||
@@ -90,3 +90,16 @@ class TestFrontendRemoval:
|
||||
def test_imgui_frontend_is_present(self):
|
||||
assert (CABANA_DIR / "ui" / "app.cc").is_file()
|
||||
assert (CABANA_DIR / "ui" / "main.cc").is_file()
|
||||
|
||||
def test_stream_selector_stays_in_main_window(self):
|
||||
app = read("ui/app.cc")
|
||||
assert "io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable" not in app
|
||||
|
||||
|
||||
class TestReplayVideo:
|
||||
def test_iqpilot_camera_index_services_are_replayed(self):
|
||||
source = read("streams/replaystream.cc")
|
||||
for service in ("roadEncodeIdx", "driverEncodeIdx", "wideRoadEncodeIdx"):
|
||||
assert f'"{service}"' in source
|
||||
assert '"narrowRoadEncodeIdx"' not in source
|
||||
assert '"cabinEncodeIdx"' not in source
|
||||
|
||||
@@ -144,8 +144,6 @@ public:
|
||||
ImPlot::CreateContext();
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
|
||||
io.ConfigViewportsNoDecoration = false;
|
||||
io.IniFilename = nullptr;
|
||||
io.LogFilename = nullptr;
|
||||
if (!ImGui_ImplGlfw_InitForOpenGL(window, true)) {
|
||||
|
||||
@@ -15,19 +15,19 @@
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
void OpenReplayWidget::draw() {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextDisabled("Replay a local or Konn3kt route with optional camera streams.");
|
||||
ImGui::Spacing();
|
||||
ImGui::TextUnformatted("Route");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-250.0f);
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
inputText("##route", &route_, "Enter route name or browse for local/remote route");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Remote route...")) {
|
||||
const float route_button_width = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
if (ImGui::Button("Browse Konn3kt routes", ImVec2(route_button_width, 34.0f))) {
|
||||
routes_dialog_.open(utils::guarded(alive_, [this](bool accepted, const std::string &route) {
|
||||
if (accepted) route_ = route;
|
||||
}));
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Local route...")) {
|
||||
if (ImGui::Button("Choose local route", ImVec2(-1.0f, 34.0f))) {
|
||||
FileDialog::getExistingDirectory("Open Local Route", settings.last_route_dir, utils::guarded(alive_, [this](const std::string &dir) {
|
||||
if (!dir.empty()) {
|
||||
route_ = dir;
|
||||
@@ -35,6 +35,8 @@ void OpenReplayWidget::draw() {
|
||||
}
|
||||
}));
|
||||
}
|
||||
ImGui::Spacing();
|
||||
ImGui::SeparatorText("Camera streams");
|
||||
checkBox("Road camera", &cameras_[0]);
|
||||
ImGui::SameLine();
|
||||
checkBox("Driver camera", &cameras_[1]);
|
||||
@@ -126,6 +128,8 @@ void OpenPandaWidget::buildConfigForm() {
|
||||
}
|
||||
|
||||
void OpenPandaWidget::draw() {
|
||||
ImGui::TextDisabled("Connect directly to a Panda and configure each CAN bus.");
|
||||
ImGui::Spacing();
|
||||
if (already_connected_) {
|
||||
ImGui::Text("Already connected to %s.", can->routeName().c_str());
|
||||
ImGui::TextUnformatted("Close the current connection via [File menu -> Close Stream] before connecting to another Panda.");
|
||||
@@ -184,6 +188,8 @@ std::unique_ptr<AbstractStream> OpenPandaWidget::open() {
|
||||
}
|
||||
|
||||
void OpenDeviceWidget::draw() {
|
||||
ImGui::TextDisabled("Connect to a running IQ.Pilot instance or a local message queue.");
|
||||
ImGui::Spacing();
|
||||
ImGui::RadioButton("MSGQ", &mode_, 0);
|
||||
ImGui::RadioButton("ZMQ", &mode_, 1);
|
||||
ImGui::RadioButton("Bridge", &mode_, 2);
|
||||
@@ -226,6 +232,8 @@ void OpenSocketCanWidget::refreshDevices() {
|
||||
}
|
||||
|
||||
void OpenSocketCanWidget::draw() {
|
||||
ImGui::TextDisabled("Read CAN traffic from a Linux SocketCAN interface.");
|
||||
ImGui::Spacing();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Device");
|
||||
ImGui::SameLine();
|
||||
@@ -248,7 +256,6 @@ std::unique_ptr<AbstractStream> OpenSocketCanWidget::open() {
|
||||
void StreamSelector::open(Callback on_done) {
|
||||
on_done_ = std::move(on_done);
|
||||
open_ = true;
|
||||
popup_.reset();
|
||||
first_frame_ = true;
|
||||
dbc_file_.clear();
|
||||
widgets_.clear();
|
||||
@@ -264,32 +271,65 @@ void StreamSelector::open(Callback on_done) {
|
||||
|
||||
void StreamSelector::draw() {
|
||||
if (!open_) return;
|
||||
if (!beginDialog("Open stream", &popup_, ImVec2(640.0f, 0.0f))) return;
|
||||
const ImGuiViewport *viewport = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(viewport->WorkPos);
|
||||
ImGui::SetNextWindowSize(viewport->WorkSize);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
const ImGuiWindowFlags page_flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoDocking;
|
||||
if (!ImGui::Begin("##stream_selector_page", nullptr, page_flags)) {
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(2);
|
||||
return;
|
||||
}
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
const ImVec2 available = ImGui::GetContentRegionAvail();
|
||||
const ImVec2 card_size(std::clamp(available.x - 80.0f, 680.0f, 920.0f),
|
||||
std::clamp(available.y - 80.0f, 470.0f, 570.0f));
|
||||
ImGui::SetCursorPos(ImVec2(std::max(24.0f, (available.x - card_size.x) * 0.5f),
|
||||
std::max(24.0f, (available.y - card_size.y) * 0.5f)));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 12.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(28.0f, 24.0f));
|
||||
ImGui::BeginChild("##stream_selector_card", card_size, ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollbar);
|
||||
ImGui::PopStyleVar(3);
|
||||
|
||||
ImGui::PushFont(boldFont(), 26.0f);
|
||||
ImGui::TextUnformatted("Open Cabana");
|
||||
ImGui::PopFont();
|
||||
ImGui::TextDisabled("Choose a data source to inspect CAN traffic, signals, and video.");
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
|
||||
AbstractOpenStreamWidget *current = nullptr;
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(16.0f, 7.0f));
|
||||
if (ImGui::BeginTabBar("streams")) {
|
||||
for (auto &w : widgets_) {
|
||||
|
||||
ImGuiTabItemFlags tab_flags = (first_frame_ && w == widgets_.front()) ? ImGuiTabItemFlags_SetSelected : 0;
|
||||
if (ImGui::BeginTabItem(w->title(), nullptr, tab_flags)) {
|
||||
current = w.get();
|
||||
ImGui::BeginChild("tab", ImVec2(0, 130.0f));
|
||||
const float content_height = std::max(170.0f, card_size.y - 300.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(18.0f, 16.0f));
|
||||
ImGui::BeginChild("tab", ImVec2(0, content_height), ImGuiChildFlags_Borders);
|
||||
w->draw();
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
first_frame_ = false;
|
||||
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("dbc File");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-90.0f);
|
||||
ImGui::SeparatorText("DBC file");
|
||||
ImGui::SetNextItemWidth(-120.0f);
|
||||
inputText("##dbc", &dbc_file_, "Choose a dbc file to open", ImGuiInputTextFlags_ReadOnly);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Browse...")) {
|
||||
if (ImGui::Button("Browse...", ImVec2(-1.0f, 0.0f))) {
|
||||
FileDialog::getOpenFileName("Open File", settings.last_dir, ".dbc", [this](const std::string &fn) {
|
||||
if (!fn.empty()) {
|
||||
dbc_file_ = fn;
|
||||
@@ -302,7 +342,13 @@ void StreamSelector::draw() {
|
||||
bool accepted = false, rejected = false;
|
||||
std::unique_ptr<AbstractStream> stream;
|
||||
bool open_clicked = false;
|
||||
dialogButtons("Open", &open_clicked, &rejected, current != nullptr && current->openEnabled());
|
||||
const float open_width = 180.0f;
|
||||
if (ImGui::Button("Cancel", ImVec2(100.0f, 38.0f))) rejected = true;
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x - open_width);
|
||||
ImGui::BeginDisabled(current == nullptr || !current->openEnabled());
|
||||
if (ImGui::Button("Open source", ImVec2(open_width, 38.0f))) open_clicked = true;
|
||||
ImGui::EndDisabled();
|
||||
if (open_clicked) {
|
||||
if (stream = current->open(); stream) accepted = true;
|
||||
}
|
||||
@@ -312,8 +358,8 @@ void StreamSelector::draw() {
|
||||
FileDialog::draw();
|
||||
MessageBox::draw();
|
||||
|
||||
if (accepted || rejected) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
ImGui::EndChild();
|
||||
ImGui::End();
|
||||
if (accepted || rejected) {
|
||||
open_ = false;
|
||||
widgets_.clear();
|
||||
|
||||
@@ -98,7 +98,6 @@ public:
|
||||
|
||||
private:
|
||||
bool open_ = false;
|
||||
PopupOwner popup_;
|
||||
bool first_frame_ = false;
|
||||
std::string dbc_file_;
|
||||
std::vector<std::unique_ptr<AbstractOpenStreamWidget>> widgets_;
|
||||
|
||||
@@ -44,6 +44,20 @@ def host():
|
||||
|
||||
class TestFileDownload:
|
||||
|
||||
def test_head_connection_released(self, monkeypatch):
|
||||
class Response:
|
||||
status = 200
|
||||
headers = {"content-length": "4"}
|
||||
released = False
|
||||
|
||||
def release_conn(self):
|
||||
self.released = True
|
||||
|
||||
response = Response()
|
||||
monkeypatch.setattr(URLFile, "_request", lambda self, method, url, headers=None: response)
|
||||
assert URLFile("https://example.com/test").get_length_online() == 4
|
||||
assert response.released
|
||||
|
||||
def test_pipeline_defaults(self, host):
|
||||
# TODO: parameterize the defaults so we don't rely on hard-coded values in xx
|
||||
|
||||
|
||||
@@ -128,10 +128,13 @@ class URLFile:
|
||||
|
||||
def get_length_online(self) -> int:
|
||||
response = self._request('HEAD', self._url)
|
||||
if not (200 <= response.status <= 299):
|
||||
return -1
|
||||
length = response.headers.get('content-length', 0)
|
||||
return int(length)
|
||||
try:
|
||||
if not (200 <= response.status <= 299):
|
||||
return -1
|
||||
length = response.headers.get('content-length', 0)
|
||||
return int(length)
|
||||
finally:
|
||||
response.release_conn()
|
||||
|
||||
def get_length(self) -> int:
|
||||
if self._length is not None:
|
||||
|
||||
@@ -40,6 +40,36 @@ def write_rlog(path: Path, n_frames: int = 200):
|
||||
f.write(msg.to_bytes())
|
||||
|
||||
|
||||
def write_video_rlog(path: Path, n_frames: int):
|
||||
with open(path, "wb") as f:
|
||||
cp = messaging.new_message('carParams')
|
||||
cp.logMonoTime = 1_000_000_000
|
||||
cp.carParams.carFingerprint = "TOYOTA_RAV4_TSS2"
|
||||
cp.carParams.brand = "toyota"
|
||||
f.write(cp.to_bytes())
|
||||
|
||||
for i in range(n_frames):
|
||||
timestamp = 1_000_000_000 + i * 50_000_000
|
||||
msg = messaging.new_message('can', 1)
|
||||
msg.logMonoTime = timestamp
|
||||
msg.can[0].address = 0x1D2
|
||||
msg.can[0].src = 0
|
||||
msg.can[0].dat = bytes([i % 256] * 8)
|
||||
f.write(msg.to_bytes())
|
||||
|
||||
idx = messaging.new_message('roadEncodeIdx')
|
||||
idx.logMonoTime = timestamp
|
||||
idx.roadEncodeIdx.frameId = i
|
||||
idx.roadEncodeIdx.type = 'fullHEVC'
|
||||
idx.roadEncodeIdx.encodeId = i
|
||||
idx.roadEncodeIdx.segmentNum = 0
|
||||
idx.roadEncodeIdx.segmentId = i
|
||||
idx.roadEncodeIdx.segmentIdEncode = i
|
||||
idx.roadEncodeIdx.timestampSof = timestamp
|
||||
idx.roadEncodeIdx.timestampEof = timestamp + 10_000_000
|
||||
f.write(idx.to_bytes())
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def local_route(tmp_path_factory):
|
||||
data_dir = tmp_path_factory.mktemp("routes")
|
||||
@@ -50,6 +80,25 @@ def local_route(tmp_path_factory):
|
||||
return data_dir
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def local_video_route(tmp_path_factory):
|
||||
data_dir = tmp_path_factory.mktemp("video_routes")
|
||||
seg_dir = data_dir / f"{DONGLE_ID}|{TIMESTAMP}--0"
|
||||
seg_dir.mkdir()
|
||||
frame_count = 20
|
||||
result = subprocess.run([
|
||||
"ffmpeg", "-hide_banner", "-loglevel", "error", "-f", "lavfi",
|
||||
"-i", "testsrc=size=320x180:rate=20", "-frames:v", str(frame_count),
|
||||
"-pix_fmt", "yuv420p", "-c:v", "libx265", "-preset", "ultrafast",
|
||||
"-x265-params", "pools=1:frame-threads=1:log-level=error", "-f", "hevc",
|
||||
str(seg_dir / "fcamera.hevc"),
|
||||
], capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
pytest.skip(result.stderr)
|
||||
write_video_rlog(seg_dir / "rlog", frame_count)
|
||||
return data_dir
|
||||
|
||||
|
||||
def run(cmd, timeout=180):
|
||||
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, env=os.environ.copy(),
|
||||
cwd=TOOLS_DIR.parent)
|
||||
@@ -62,6 +111,33 @@ def cabana_command(*args):
|
||||
return command
|
||||
|
||||
|
||||
def cabana_output_until(args, expected, timeout=60):
|
||||
master, slave = pty.openpty()
|
||||
proc = subprocess.Popen(cabana_command(*args), stdout=slave, stderr=slave,
|
||||
env=os.environ.copy(), cwd=TOOLS_DIR.parent)
|
||||
os.close(slave)
|
||||
output = bytearray()
|
||||
deadline = time.monotonic() + timeout
|
||||
try:
|
||||
while time.monotonic() < deadline:
|
||||
ready, _, _ = select.select([master], [], [], min(1, deadline - time.monotonic()))
|
||||
if not ready:
|
||||
if proc.poll() is not None:
|
||||
break
|
||||
continue
|
||||
try:
|
||||
output.extend(os.read(master, 4096))
|
||||
except OSError:
|
||||
break
|
||||
if expected.encode() in output:
|
||||
break
|
||||
finally:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
os.close(master)
|
||||
return output.decode(errors="replace")
|
||||
|
||||
|
||||
def test_jotpluggler_renders_a_local_route(local_route, tmp_path):
|
||||
assert JOTPLUGGLER_BIN.exists(), "jotpluggler not built"
|
||||
out = tmp_path / "plot.png"
|
||||
@@ -74,38 +150,20 @@ def test_jotpluggler_renders_a_local_route(local_route, tmp_path):
|
||||
|
||||
def test_cabana_loads_a_local_route(local_route):
|
||||
assert CABANA_BIN.exists(), "cabana not built"
|
||||
master, slave = pty.openpty()
|
||||
proc = subprocess.Popen(cabana_command("--data_dir", str(local_route), "--no-vipc", ROUTE),
|
||||
stdout=slave, stderr=slave,
|
||||
env=os.environ.copy(), cwd=TOOLS_DIR.parent)
|
||||
os.close(slave)
|
||||
loaded = f"loaded route {ROUTE} with 2 valid segments"
|
||||
output = bytearray()
|
||||
deadline = time.monotonic() + 60
|
||||
try:
|
||||
while time.monotonic() < deadline:
|
||||
ready, _, _ = select.select([master], [], [], min(1, deadline - time.monotonic()))
|
||||
if not ready:
|
||||
if proc.poll() is not None:
|
||||
break
|
||||
continue
|
||||
try:
|
||||
output.extend(os.read(master, 4096))
|
||||
except OSError:
|
||||
break
|
||||
if loaded.encode() in output:
|
||||
break
|
||||
finally:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
os.close(master)
|
||||
|
||||
out = output.decode(errors="replace")
|
||||
out = cabana_output_until(("--data_dir", str(local_route), "--no-vipc", ROUTE), loaded)
|
||||
assert "failed to load route" not in out, out
|
||||
assert "invalid route format" not in out, out
|
||||
assert loaded in out, out
|
||||
|
||||
|
||||
def test_cabana_replays_local_video(local_video_route):
|
||||
expected = "camera[0] vipc send #1"
|
||||
out = cabana_output_until(("--data_dir", str(local_video_route), ROUTE), expected)
|
||||
assert "failed to get frame" not in out, out
|
||||
assert expected in out, out
|
||||
|
||||
|
||||
def test_replay_logreader_reports_load_stats(local_route):
|
||||
assert shutil.which("python3") is not None
|
||||
header = (TOOLS_DIR / "replay" / "logreader.h").read_text()
|
||||
|
||||
@@ -70,7 +70,7 @@ from iqpilot.system.ui.iqwidgets.widgets.list_view import (
|
||||
Spacer,
|
||||
)
|
||||
from iqpilot.system.ui.iqwidgets.widgets.list_view import IQListItem
|
||||
from iqpilot.system.ui.iqwidgets.widgets.list_view import IQListItem, IQMultipleButtonAction, IQToggleAction, IQLineSeparator
|
||||
from iqpilot.system.ui.iqwidgets.widgets.list_view import IQListItem, IQMultipleButtonAction, IQToggleAction, IQLineSeparator, toggle_item_iq
|
||||
from iqpilot.system.ui.iqwidgets.widgets.list_view import button_item, toggle_item
|
||||
from iqpilot.system.ui.iqwidgets.widgets.list_view import NoticeModal
|
||||
from iqpilot.system.ui.iqwidgets.widgets.list_view import PickerDialog, PickerItem, PickerGroup
|
||||
@@ -1655,6 +1655,14 @@ class ModelsLayout(Widget):
|
||||
)
|
||||
self.big_model_item.action_item.set_value(self._big_model_value())
|
||||
|
||||
self.small_on_mac_item = toggle_item_iq(
|
||||
lambda: tr("Active Model on eMac"),
|
||||
tr("Run the selected small model on the Mac instead of a big model."),
|
||||
initial_state=ui_state.params.get_bool("IQEmacSmallModel"),
|
||||
callback=self._on_small_on_mac_toggled,
|
||||
param="IQEmacSmallModel",
|
||||
)
|
||||
|
||||
self.supercombo_label = progress_item(tr("Combined Model"))
|
||||
self.vision_label = progress_item(tr("Vision Weights"))
|
||||
self.policy_label = progress_item(tr("Policy Weights"))
|
||||
@@ -1671,7 +1679,7 @@ class ModelsLayout(Widget):
|
||||
self.redownload_item = button_item(lambda: tr("Redownload Current Model"), lambda: tr("REDOWNLOAD"), "", self._redownload_model)
|
||||
self.cancel_download_item = button_item(tr("Stop Download"), tr("Cancel"), "", self._cancel_model_request)
|
||||
|
||||
self.items = [self.current_model_item, self.big_model_item, self.cancel_download_item, self.supercombo_label, self.vision_label,
|
||||
self.items = [self.current_model_item, self.big_model_item, self.small_on_mac_item, self.cancel_download_item, self.supercombo_label, self.vision_label,
|
||||
self.policy_label, self.redownload_item, self.refresh_item, self.clear_cache_item]
|
||||
|
||||
def _is_downloading(self):
|
||||
@@ -1921,10 +1929,18 @@ class ModelsLayout(Widget):
|
||||
get_folders_fn=self._get_folders, on_exit=self._on_model_selected)
|
||||
gui_app.set_modal_overlay(self.model_dialog, callback=self._on_model_selected)
|
||||
|
||||
def _on_small_on_mac_toggled(self, state: bool) -> None:
|
||||
p = ui_state.params
|
||||
p.put_bool("IQEmacSmallModel", bool(state))
|
||||
p.put_bool("IQEmacEnabled", True if state else bool(p.get("IQEmacModel")))
|
||||
self.big_model_item.action_item.set_value(self._big_model_value())
|
||||
|
||||
@staticmethod
|
||||
def _big_model_value() -> str:
|
||||
if not ui_state.params.get_bool("IQEmacEnabled"):
|
||||
return tr("Off")
|
||||
if ui_state.params.get_bool("IQEmacSmallModel"):
|
||||
return tr("Active model (eMac)")
|
||||
key = ui_state.params.get("IQEmacModel")
|
||||
key = key.decode() if isinstance(key, bytes) else (key or "")
|
||||
return _big_model_label(key) if key in [n for n, _ in _big_model_options()] else tr("Off")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
import math
|
||||
import time
|
||||
|
||||
import pyray as rl
|
||||
@@ -17,6 +18,9 @@ from iqpilot.system.ui.widgets import Widget
|
||||
|
||||
_POLL_S = 1.0
|
||||
_FONT_SIZE = 70
|
||||
_ICON_H = 76
|
||||
_ICONS = {"MAC": ("mac", 62 / 46), "GPU": ("egpu", 1.0)}
|
||||
_GREY = rl.Color(165, 165, 170, 235)
|
||||
|
||||
|
||||
class EmacStatusRenderer(Widget):
|
||||
@@ -27,6 +31,21 @@ class EmacStatusRenderer(Widget):
|
||||
self._last_poll = 0.0
|
||||
self._label = ""
|
||||
self._state = SourceState.HIDDEN
|
||||
self._icons: dict[str, dict[str, rl.Texture]] = {}
|
||||
|
||||
def _icon_set(self, label: str) -> dict[str, rl.Texture] | None:
|
||||
spec = _ICONS.get(label)
|
||||
if spec is None:
|
||||
return None
|
||||
if label not in self._icons:
|
||||
base, aspect = spec
|
||||
w = int(_ICON_H * aspect)
|
||||
self._icons[label] = {
|
||||
"base": gui_app.texture(f"icons_mici/{base}.png", w, _ICON_H),
|
||||
"green": gui_app.texture(f"icons_mici/{base}_green.png", w, _ICON_H),
|
||||
"orange": gui_app.texture(f"icons_mici/{base}_orange.png", int(w * 1.26), _ICON_H),
|
||||
}
|
||||
return self._icons[label]
|
||||
|
||||
def update(self):
|
||||
now = time.monotonic()
|
||||
@@ -38,7 +57,25 @@ class EmacStatusRenderer(Widget):
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if self._state == SourceState.HIDDEN:
|
||||
return
|
||||
size = measure_text_cached(self._font, self._label, _FONT_SIZE)
|
||||
x = rect.x + UI_BORDER_SIZE + BTN_SIZE // 2 - size.x / 2
|
||||
y = rect.y + rect.height / 2 - size.y / 2
|
||||
draw_source_label(self._font, self._label, self._state, rl.Vector2(x, y), _FONT_SIZE)
|
||||
icons = self._icon_set(self._label)
|
||||
if icons is None:
|
||||
size = measure_text_cached(self._font, self._label, _FONT_SIZE)
|
||||
x = rect.x + UI_BORDER_SIZE + BTN_SIZE // 2 - size.x / 2
|
||||
y = rect.y + rect.height / 2 - size.y / 2
|
||||
draw_source_label(self._font, self._label, self._state, rl.Vector2(x, y), _FONT_SIZE)
|
||||
return
|
||||
if self._state == SourceState.ACTIVE:
|
||||
tex, tint = icons["green"], rl.Color(255, 255, 255, 255)
|
||||
elif self._state == SourceState.FAILED:
|
||||
tex, tint = icons["orange"], rl.Color(255, 255, 255, 255)
|
||||
elif self._state == SourceState.CROSSED:
|
||||
tex, tint = icons["base"], rl.Color(255, 255, 255, 165)
|
||||
else:
|
||||
pulse = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))
|
||||
tex, tint = icons["base"], rl.Color(_GREY.r, _GREY.g, _GREY.b, int(_GREY.a * pulse))
|
||||
x = int(rect.x + UI_BORDER_SIZE + BTN_SIZE // 2 - tex.width / 2)
|
||||
y = int(rect.y + rect.height / 2 - tex.height / 2)
|
||||
rl.draw_texture(tex, x, y, tint)
|
||||
if self._state == SourceState.CROSSED:
|
||||
cy = y + tex.height // 2
|
||||
rl.draw_line_ex(rl.Vector2(x - 4, cy), rl.Vector2(x + tex.width + 4, cy), 4, rl.Color(255, 255, 255, 165))
|
||||
|
||||
Reference in New Issue
Block a user