IQ.Pilot Release Commit @ d2ce8a8
This commit is contained in:
@@ -1590,6 +1590,31 @@ _ACTIVE_BUNDLE_KEY = "ModelManager_ActiveBundle"
|
||||
_DOWNLOAD_INDEX_KEY = "ModelManager_DownloadIndex"
|
||||
_RUNNER_CACHE_KEY = "ModelRunnerTypeCache"
|
||||
|
||||
def _big_model_options() -> list[tuple[str, str]]:
|
||||
try:
|
||||
from iqpilot.selfdrive.iqmodeld.emac_model_meta import big_models
|
||||
return big_models(ui_state.params)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _big_model_label(key: str) -> str:
|
||||
for name, display in _big_model_options():
|
||||
if name == key:
|
||||
return display
|
||||
return key
|
||||
|
||||
|
||||
def _refresh_big_catalog() -> None:
|
||||
def worker():
|
||||
try:
|
||||
from iqpilot.selfdrive.iqmodeld.emac_model_meta import refresh_catalog
|
||||
refresh_catalog(ui_state.params)
|
||||
except Exception:
|
||||
pass
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
|
||||
class ModelsLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -1599,6 +1624,7 @@ class ModelsLayout(Widget):
|
||||
self.download_status = None
|
||||
self.prev_download_status = None
|
||||
self.model_dialog = None
|
||||
self._big_model_dialog = None
|
||||
self.last_cache_calc_time = 0
|
||||
|
||||
self._initialize_items()
|
||||
@@ -1614,6 +1640,14 @@ class ModelsLayout(Widget):
|
||||
callback=self._handle_current_model_clicked
|
||||
)
|
||||
|
||||
self.big_model_item = button_item(
|
||||
lambda: tr("Big Model"),
|
||||
lambda: tr("CHANGE"),
|
||||
tr("Only works with external compute connected over USB."),
|
||||
self._handle_big_model_clicked,
|
||||
)
|
||||
self.big_model_item.action_item.set_value(self._big_model_value())
|
||||
|
||||
self.supercombo_label = progress_item(tr("Combined Model"))
|
||||
self.vision_label = progress_item(tr("Vision Weights"))
|
||||
self.policy_label = progress_item(tr("Policy Weights"))
|
||||
@@ -1630,7 +1664,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.cancel_download_item, self.supercombo_label, self.vision_label,
|
||||
self.items = [self.current_model_item, self.big_model_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):
|
||||
@@ -1880,7 +1914,40 @@ 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)
|
||||
|
||||
@staticmethod
|
||||
def _big_model_value() -> str:
|
||||
if not ui_state.params.get_bool("IQEmacEnabled"):
|
||||
return tr("Off")
|
||||
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")
|
||||
|
||||
def _handle_big_model_clicked(self):
|
||||
options = _big_model_options()
|
||||
if len(options) <= 1:
|
||||
_refresh_big_catalog()
|
||||
keys = [n for n, _ in options]
|
||||
labels = [tr("Off")] + [d for _, d in options]
|
||||
self._big_model_dialog = MultiOptionDialog(tr("Big Model"), labels, self._big_model_value())
|
||||
|
||||
def handle_selection(result):
|
||||
if result == DialogResult.CONFIRM and self._big_model_dialog is not None and self._big_model_dialog.selection:
|
||||
selected = self._big_model_dialog.selection
|
||||
if selected == tr("Off"):
|
||||
ui_state.params.put_bool("IQEmacEnabled", False)
|
||||
else:
|
||||
for key in keys:
|
||||
if _big_model_label(key) == selected:
|
||||
ui_state.params.put("IQEmacModel", key)
|
||||
ui_state.params.put_bool("IQEmacEnabled", True)
|
||||
break
|
||||
self.big_model_item.action_item.set_value(self._big_model_value())
|
||||
self._big_model_dialog = None
|
||||
|
||||
gui_app.set_modal_overlay(self._big_model_dialog, callback=handle_selection)
|
||||
|
||||
def _on_refresh_models(self):
|
||||
_refresh_big_catalog()
|
||||
ui_state.params.put("ModelManager_LastSyncTime", 0)
|
||||
self._refreshing = True
|
||||
self._refresh_start = time.monotonic()
|
||||
|
||||
43
iqpilot/ui/mici/onroad/emac_source.py
Normal file
43
iqpilot/ui/mici/onroad/emac_source.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.ui.onroad.big_model_status import SourceState, draw_source_label, resolve_source
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from iqpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
|
||||
_POLL_S = 1.0
|
||||
_FONT_SIZE = 44
|
||||
_WHEEL_H = 50
|
||||
_MARGIN_R = 12
|
||||
_MARGIN_B = 14
|
||||
|
||||
|
||||
class EmacSourceIndicator:
|
||||
def __init__(self):
|
||||
self._params = Params()
|
||||
self._font = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
self._last_poll = 0.0
|
||||
self._label = ""
|
||||
self._state = SourceState.HIDDEN
|
||||
|
||||
def update(self) -> None:
|
||||
now = time.monotonic()
|
||||
if now - self._last_poll < _POLL_S:
|
||||
return
|
||||
self._last_poll = now
|
||||
self._label, self._state = resolve_source(self._params, ui_state.engaged)
|
||||
|
||||
def render(self, rect: rl.Rectangle) -> None:
|
||||
if self._state == SourceState.HIDDEN:
|
||||
return
|
||||
size = measure_text_cached(self._font, self._label, _FONT_SIZE)
|
||||
x = rect.x + rect.width - _MARGIN_R - size.x
|
||||
y = rect.y + rect.height - _MARGIN_B - (_WHEEL_H + size.y) / 2
|
||||
draw_source_label(self._font, self._label, self._state, rl.Vector2(x, y), _FONT_SIZE)
|
||||
@@ -5,11 +5,12 @@ import pyray as rl
|
||||
|
||||
from iqpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer
|
||||
from iqpilot.ui.onroad.hud_overlays import IQBlindSpotOverlay
|
||||
from iqpilot.ui.mici.onroad.emac_source import EmacSourceIndicator
|
||||
|
||||
class IQMiciHudRenderer(HudRenderer):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._overlays = [IQBlindSpotOverlay()]
|
||||
self._overlays = [IQBlindSpotOverlay(), EmacSourceIndicator()]
|
||||
|
||||
def _update_state(self) -> None:
|
||||
super()._update_state()
|
||||
|
||||
71
iqpilot/ui/onroad/big_model_status.py
Normal file
71
iqpilot/ui/onroad/big_model_status.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import math
|
||||
from enum import IntEnum
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
|
||||
class SourceState(IntEnum):
|
||||
HIDDEN = 0
|
||||
LOADING = 1
|
||||
ACTIVE = 2
|
||||
FAILED = 3
|
||||
CROSSED = 4
|
||||
|
||||
|
||||
_GREEN = rl.Color(46, 204, 113, 255)
|
||||
_ORANGE = rl.Color(255, 115, 0, 255)
|
||||
_WHITE = rl.Color(255, 255, 255, 255)
|
||||
|
||||
|
||||
def _emac_state(params: Params, engaged: bool) -> SourceState:
|
||||
if not params.get_bool("MacModelReachable"):
|
||||
return SourceState.HIDDEN
|
||||
if params.get_bool("MacModelActive"):
|
||||
return SourceState.ACTIVE
|
||||
if params.get_bool("MacModelFailed"):
|
||||
return SourceState.CROSSED if engaged else SourceState.FAILED
|
||||
return SourceState.LOADING
|
||||
|
||||
|
||||
def _egpu_state(params: Params, engaged: bool) -> SourceState:
|
||||
if not params.get_bool("UsbGpuPresent"):
|
||||
return SourceState.HIDDEN
|
||||
if params.get_bool("UsbGpuActive"):
|
||||
return SourceState.ACTIVE
|
||||
if params.get_bool("UsbGpuFailed"):
|
||||
return SourceState.CROSSED if engaged else SourceState.FAILED
|
||||
return SourceState.LOADING
|
||||
|
||||
|
||||
def resolve_source(params: Params, engaged: bool) -> tuple[str, SourceState]:
|
||||
if params.get_bool("IQEmacEnabled"):
|
||||
return "MAC", _emac_state(params, engaged)
|
||||
if params.get_bool("UsbGpuPresent") or params.get_bool("IQEgpuEnabled"):
|
||||
return "GPU", _egpu_state(params, engaged)
|
||||
return "", SourceState.HIDDEN
|
||||
|
||||
|
||||
def draw_source_label(font: rl.Font, label: str, state: SourceState,
|
||||
pos: rl.Vector2, font_size: int) -> None:
|
||||
if state == SourceState.HIDDEN or not label:
|
||||
return
|
||||
if state == SourceState.ACTIVE:
|
||||
color, opacity, strike = _GREEN, 1.0, False
|
||||
elif state == SourceState.FAILED:
|
||||
color, opacity, strike = _ORANGE, 1.0, False
|
||||
elif state == SourceState.CROSSED:
|
||||
color, opacity, strike = _WHITE, 0.65, True
|
||||
else:
|
||||
color, opacity, strike = _WHITE, 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)), False
|
||||
|
||||
col = rl.Color(color.r, color.g, color.b, int(255 * opacity))
|
||||
rl.draw_text_ex(font, label, pos, font_size, 0, col)
|
||||
if strike:
|
||||
size = measure_text_cached(font, label, font_size)
|
||||
y = int(pos.y + size.y / 2)
|
||||
rl.draw_line_ex(rl.Vector2(pos.x - 2, y), rl.Vector2(pos.x + size.x + 2, y), 4, col)
|
||||
44
iqpilot/ui/onroad/emac_status.py
Normal file
44
iqpilot/ui/onroad/emac_status.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.ui.onroad.big_model_status import SourceState, draw_source_label, resolve_source
|
||||
from iqpilot.selfdrive.ui import UI_BORDER_SIZE
|
||||
from iqpilot.selfdrive.ui.onroad.driver_state import BTN_SIZE
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from iqpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from iqpilot.system.ui.widgets import Widget
|
||||
|
||||
_POLL_S = 1.0
|
||||
_FONT_SIZE = 70
|
||||
|
||||
|
||||
class EmacStatusRenderer(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._font = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
self._last_poll = 0.0
|
||||
self._label = ""
|
||||
self._state = SourceState.HIDDEN
|
||||
|
||||
def update(self):
|
||||
now = time.monotonic()
|
||||
if now - self._last_poll < _POLL_S:
|
||||
return
|
||||
self._last_poll = now
|
||||
self._label, self._state = resolve_source(self._params, ui_state.engaged)
|
||||
|
||||
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)
|
||||
@@ -16,6 +16,7 @@ from iqpilot.ui.onroad.hud_overlays import (
|
||||
)
|
||||
from iqpilot.ui.onroad.nav_map_panel import NavMapPanel
|
||||
from iqpilot.ui.onroad.soft_warning import SoftWarningRenderer
|
||||
from iqpilot.ui.onroad.emac_status import EmacStatusRenderer
|
||||
|
||||
ENABLE_FLOATING_NAV_MAP_PANEL = False
|
||||
ENABLE_SPLIT_NAV_MAP_PANEL = True
|
||||
@@ -25,6 +26,7 @@ class IQHudRenderer(HudRenderer):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.developer_ui = IQDevMetricsOverlay()
|
||||
self.emac_status = EmacStatusRenderer()
|
||||
self.nav_map_panel = NavMapPanel()
|
||||
self.road_name_renderer = RoadNameRenderer()
|
||||
self.rocket_fuel = IQAccelBar()
|
||||
@@ -38,6 +40,7 @@ class IQHudRenderer(HudRenderer):
|
||||
super()._update_state()
|
||||
if ENABLE_FLOATING_NAV_MAP_PANEL or ENABLE_SPLIT_NAV_MAP_PANEL:
|
||||
self.nav_map_panel.update()
|
||||
self.emac_status.update()
|
||||
self.road_name_renderer.update()
|
||||
self.speed_limit_renderer.update()
|
||||
has_limit = self.speed_limit_renderer.speed_limit_valid or self.speed_limit_renderer.speed_limit_last_valid
|
||||
@@ -65,6 +68,7 @@ class IQHudRenderer(HudRenderer):
|
||||
self.developer_ui.render(rect)
|
||||
if ENABLE_FLOATING_NAV_MAP_PANEL:
|
||||
self.nav_map_panel.render(rect)
|
||||
self.emac_status.render(rect)
|
||||
self.road_name_renderer.render(torque_rect)
|
||||
self.turn_signal_controller.render(rect)
|
||||
self.soft_warning_renderer.render(rect)
|
||||
|
||||
66
iqpilot/ui/tests/test_big_model_status.py
Normal file
66
iqpilot/ui/tests/test_big_model_status.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
|
||||
The BIG (tici/tizi) and SMALL (mici) onroad source indicators share one
|
||||
resolver (comma PR #38492 states, backend-aware label). Pin its truth table.
|
||||
"""
|
||||
from iqpilot.ui.onroad.big_model_status import SourceState, resolve_source
|
||||
|
||||
|
||||
class _FakeParams:
|
||||
def __init__(self, **flags):
|
||||
self._flags = flags
|
||||
|
||||
def get_bool(self, key: str) -> bool:
|
||||
return bool(self._flags.get(key, False))
|
||||
|
||||
|
||||
def _resolve(engaged=False, **flags):
|
||||
return resolve_source(_FakeParams(**flags), engaged)
|
||||
|
||||
|
||||
def test_no_backend_enabled_is_hidden():
|
||||
assert _resolve() == ("", SourceState.HIDDEN)
|
||||
|
||||
|
||||
def test_emac_label_is_mac():
|
||||
label, _ = _resolve(IQEmacEnabled=True, MacModelReachable=True, MacModelActive=True)
|
||||
assert label == "MAC"
|
||||
|
||||
|
||||
def test_egpu_label_is_gpu():
|
||||
label, _ = _resolve(IQEgpuEnabled=True, UsbGpuPresent=True, UsbGpuActive=True)
|
||||
assert label == "GPU"
|
||||
|
||||
|
||||
def test_emac_wins_when_both_enabled():
|
||||
label, _ = _resolve(IQEmacEnabled=True, IQEgpuEnabled=True,
|
||||
MacModelReachable=True, UsbGpuPresent=True)
|
||||
assert label == "MAC"
|
||||
|
||||
|
||||
def test_egpu_states_mirror_emac():
|
||||
assert _resolve(IQEgpuEnabled=True)[1] == SourceState.HIDDEN # not present
|
||||
assert _resolve(IQEgpuEnabled=True, UsbGpuPresent=True)[1] == SourceState.LOADING
|
||||
assert _resolve(IQEgpuEnabled=True, UsbGpuPresent=True, UsbGpuActive=True)[1] == SourceState.ACTIVE
|
||||
assert _resolve(IQEgpuEnabled=True, UsbGpuPresent=True, UsbGpuFailed=True)[1] == SourceState.FAILED
|
||||
assert _resolve(engaged=True, IQEgpuEnabled=True, UsbGpuPresent=True,
|
||||
UsbGpuFailed=True)[1] == SourceState.CROSSED
|
||||
|
||||
|
||||
def test_emac_states():
|
||||
assert _resolve(IQEmacEnabled=True)[1] == SourceState.HIDDEN # unreachable
|
||||
assert _resolve(IQEmacEnabled=True, MacModelReachable=True)[1] == SourceState.LOADING
|
||||
assert _resolve(IQEmacEnabled=True, MacModelReachable=True, MacModelActive=True)[1] == SourceState.ACTIVE
|
||||
assert _resolve(IQEmacEnabled=True, MacModelReachable=True, MacModelFailed=True)[1] == SourceState.FAILED
|
||||
# engaged on small because big failed -> crossed
|
||||
assert _resolve(engaged=True, IQEmacEnabled=True, MacModelReachable=True,
|
||||
MacModelFailed=True)[1] == SourceState.CROSSED
|
||||
# active always wins over a stale failed flag
|
||||
assert _resolve(IQEmacEnabled=True, MacModelReachable=True,
|
||||
MacModelActive=True, MacModelFailed=True)[1] == SourceState.ACTIVE
|
||||
|
||||
|
||||
def test_failed_but_disconnected_is_hidden():
|
||||
# unplugged mid-drive: nothing to show, not a red/orange "failed"
|
||||
assert _resolve(IQEmacEnabled=True, MacModelReachable=False, MacModelFailed=True)[1] == SourceState.HIDDEN
|
||||
Reference in New Issue
Block a user