IQ.Pilot Release Commit @ f2a861c
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
|
||||
@@ -6,27 +6,23 @@ import threading
|
||||
import time
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.common.api import api_get
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.time_helpers import system_time_valid
|
||||
from openpilot.selfdrive.ui.lib.api_helpers import get_token
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.iqpilot.konn3kt.registration import UNREGISTERED_DONGLE_ID
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from iqpilot.common.api import api_get
|
||||
from iqpilot.common.constants import CV
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.common.swaglog import cloudlog
|
||||
from iqpilot.common.time_helpers import system_time_valid
|
||||
from iqpilot.selfdrive.ui.lib.api_helpers import get_token
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from iqpilot.konn3kt.registration import UNREGISTERED_DONGLE_ID
|
||||
from iqpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
from iqpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from iqpilot.system.ui.widgets import Widget
|
||||
|
||||
_STATS_PARAM = "ApiCache_DriveStats"
|
||||
_POLL_SECONDS = 30
|
||||
|
||||
|
||||
class _DriveStatsSource:
|
||||
"""Owns the konn3kt drive-stats fetch. Seeds from the cached param, then keeps it fresh on a
|
||||
background poll while the device is offroad and awake. Read `snapshot` for the latest data."""
|
||||
|
||||
def __init__(self):
|
||||
self._params = Params()
|
||||
self._http = requests.Session()
|
||||
@@ -54,7 +50,6 @@ class _DriveStatsSource:
|
||||
dongle_id = self._params.get("DongleId")
|
||||
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
|
||||
return
|
||||
# at boot the clock isn't NTP-synced, so the token can't be minted yet — skip quietly
|
||||
if not system_time_valid():
|
||||
return
|
||||
resp = api_get(f"v1.1/devices/{dongle_id}/stats", access_token=get_token(dongle_id), session=self._http)
|
||||
@@ -67,7 +62,7 @@ class _DriveStatsSource:
|
||||
|
||||
|
||||
class TripsLayout(Widget):
|
||||
PARAM_KEY = _STATS_PARAM # retained for external references
|
||||
PARAM_KEY = _STATS_PARAM
|
||||
UPDATE_INTERVAL = _POLL_SECONDS
|
||||
|
||||
_CARD_FILL = rl.Color(38, 40, 46, 255)
|
||||
@@ -81,10 +76,9 @@ class TripsLayout(Widget):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._source = _DriveStatsSource()
|
||||
# one shared height so the three columns line up; tinted teal at draw time
|
||||
self._ic_drives = gui_app.texture("icons_mici/wheel.png", 64, 64, keep_aspect_ratio=True)
|
||||
self._ic_distance = gui_app.texture("icons/road.png", 88, 64, keep_aspect_ratio=True)
|
||||
self._ic_hours = gui_app.texture("../../iqpilot/selfdrive/assets/icons/clock.png", 64, 64, keep_aspect_ratio=True)
|
||||
self._ic_hours = gui_app.texture("icons/clock.png", 64, 64, keep_aspect_ratio=True)
|
||||
|
||||
def __del__(self):
|
||||
self._source.close()
|
||||
@@ -105,8 +99,6 @@ class TripsLayout(Widget):
|
||||
card = rl.Rectangle(x, y, width, height)
|
||||
rl.draw_rectangle_rounded(card, 0.10, 20, self._CARD_FILL)
|
||||
rl.draw_rectangle_rounded_lines_ex(card, 0.10, 20, 2, self._CARD_EDGE)
|
||||
|
||||
# heading: teal tick + muted-teal caption
|
||||
pad = 44
|
||||
label_y = y + 36
|
||||
tick_h = 30
|
||||
@@ -114,11 +106,9 @@ class TripsLayout(Widget):
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(x + pad, label_y, 6, tick_h), 0.5, 6, self._ACCENT)
|
||||
rl.draw_text_ex(gui_app.font(FontWeight.BOLD), title,
|
||||
rl.Vector2(x + pad + 22, label_y + (tick_h - title_size) / 2), title_size, 4, self._ACCENT_DIM)
|
||||
|
||||
col_width = width / 3
|
||||
content_top = label_y + tick_h + 20
|
||||
content_bottom = y + height - 30
|
||||
|
||||
number_font = gui_app.font(FontWeight.BOLD)
|
||||
unit_font = gui_app.font(FontWeight.MEDIUM)
|
||||
number_size = 84 * FONT_SCALE
|
||||
@@ -126,8 +116,6 @@ class TripsLayout(Widget):
|
||||
unit_spacing = 2.0
|
||||
icon_gap = 16
|
||||
num_gap = 14
|
||||
|
||||
# vertical rules between the three columns
|
||||
for i in (1, 2):
|
||||
dx = x + col_width * i
|
||||
rl.draw_line_ex(rl.Vector2(dx, content_top + 4), rl.Vector2(dx, content_bottom - 4), 1, self._RULE)
|
||||
|
||||
@@ -5,14 +5,14 @@ from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.iqwidgets.widgets.list_view import IQListItem, IQToggleAction, SafeIQToggleAction
|
||||
from openpilot.system.ui.iqwidgets.widgets.list_view import OptionControl
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.network import NavButton
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
from iqpilot.system.ui.iqwidgets.widgets.list_view import IQListItem, IQToggleAction, SafeIQToggleAction
|
||||
from iqpilot.system.ui.iqwidgets.widgets.list_view import OptionControl
|
||||
from iqpilot.system.ui.widgets import Widget
|
||||
from iqpilot.system.ui.widgets.network import NavButton
|
||||
from iqpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from iqdbc.car.volkswagen.values import CAR, VolkswagenFlags
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
|
||||
|
||||
ACTIVE_CONFIDENCE_TOP = rl.Color(0x22, 0xB8, 0xB9, 0xFF)
|
||||
@@ -19,7 +19,6 @@ class IQConfidenceBall:
|
||||
if ui_state.status == UIStatus.LAT_ONLY:
|
||||
return ui_state.sm['modelV2'].meta.disengagePredictions.steerOverrideProbs
|
||||
|
||||
# UIStatus.LONG_ONLY
|
||||
return ui_state.sm['modelV2'].meta.disengagePredictions.brakeDisengageProbs
|
||||
|
||||
@staticmethod
|
||||
|
||||
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)
|
||||
@@ -3,30 +3,29 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed
|
||||
"""
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer
|
||||
from openpilot.iqpilot.ui.onroad.hud_overlays import IQBlindSpotOverlay
|
||||
|
||||
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
|
||||
from iqpilot.ui.mici.onroad.speed_limit_sign import MiciSpeedLimitSign
|
||||
|
||||
class IQMiciHudRenderer(HudRenderer):
|
||||
"""Stock Mici HUD extended with IQ.Pilot's own onroad overlays.
|
||||
|
||||
Overlays live in a list so the renderer stays overlay-agnostic — each just needs
|
||||
update()/render(rect); blind-spot state is surfaced by any overlay that exposes it.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._overlays = [IQBlindSpotOverlay()]
|
||||
self._overlays = [IQBlindSpotOverlay(), EmacSourceIndicator()]
|
||||
self._speed_limit_sign = MiciSpeedLimitSign()
|
||||
|
||||
def _update_state(self) -> None:
|
||||
super()._update_state()
|
||||
for overlay in self._overlays:
|
||||
overlay.update()
|
||||
self._speed_limit_sign.update()
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
super()._render(rect)
|
||||
for overlay in self._overlays:
|
||||
overlay.render(rect)
|
||||
self._speed_limit_sign.set_obscured(self.drawing_top_icons() or not self._can_draw_top_icons)
|
||||
self._speed_limit_sign.render(rect)
|
||||
|
||||
def _has_blind_spot_detected(self) -> bool:
|
||||
return any(getattr(overlay, "detected", False) for overlay in self._overlays)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.ui_state import UIStatus
|
||||
|
||||
IQ_LANE_LINE_COLORS = {
|
||||
UIStatus.LAT_ONLY: rl.Color(0x0C, 0x94, 0x96, 0xFF),
|
||||
UIStatus.LONG_ONLY: rl.Color(0x0C, 0x94, 0x96, 0xFF),
|
||||
}
|
||||
@@ -1,20 +1,10 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
from openpilot.iqpilot.ui.onroad.hud_overlays import RoadNameBanner
|
||||
|
||||
from iqpilot.ui.onroad.hud_overlays import RoadNameBanner
|
||||
|
||||
class RoadNameRendererMici(RoadNameBanner):
|
||||
"""Compact capsule tuned for the mici's small panel."""
|
||||
|
||||
TYPE_SIZE = 28
|
||||
FLOOR_WIDTH = 120
|
||||
SIDE_PAD = 28
|
||||
MARGIN = 200
|
||||
DROP = 8
|
||||
BAR_H = TYPE_SIZE + 14
|
||||
CURVE = 0.35
|
||||
SEGS = 8
|
||||
BACKDROP_A = 140
|
||||
INK_A = 210
|
||||
INNER_PAD = 16
|
||||
TYPE_SIZE = 18
|
||||
MARGIN = 120
|
||||
GAP = 2
|
||||
TORQUE_SCALE = 1.0
|
||||
|
||||
75
iqpilot/ui/mici/onroad/speed_limit_sign.py
Normal file
75
iqpilot/ui/mici/onroad/speed_limit_sign.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import math
|
||||
|
||||
from iqpilot.common.filter_simple import FirstOrderFilter
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
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
|
||||
_BADGE_SIDE = 24
|
||||
|
||||
|
||||
class MiciSpeedLimitSign(IQSpeedLimitOverlay):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._visible = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
|
||||
self._obscured = False
|
||||
|
||||
def set_obscured(self, obscured: bool) -> None:
|
||||
self._obscured = obscured
|
||||
|
||||
def _badge(self) -> str:
|
||||
offset = round(self.speed_limit_offset)
|
||||
return f"{offset:+d}" if offset != 0 else ""
|
||||
|
||||
def _render(self, rect):
|
||||
shown = ui_state.speed_limit_mode != 0 and ui_state.is_onroad() and not self._obscured
|
||||
alpha = self._visible.update(1.0 if shown else 0.0)
|
||||
if alpha < 1e-2:
|
||||
return
|
||||
if self.assist_state == _SL_ASSIST.preActive:
|
||||
self.assist_frame += 1
|
||||
pulse = 0.65 + 0.35 * math.sin(self.assist_frame * math.pi / gui_app.target_fps)
|
||||
alpha *= self._pulse_ema.update(pulse)
|
||||
else:
|
||||
self.assist_frame = 0
|
||||
self._pulse_ema.update(1.0)
|
||||
box = canvas.Box(rect.x + _SIGN_X, rect.y + _SIGN_Y, _SIGN_W, _SIGN_H)
|
||||
value, _, tint, has_limit = self._spec()
|
||||
badge = self._badge() if has_limit else ""
|
||||
(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
|
||||
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))
|
||||
if badge:
|
||||
br = _BADGE_SIDE / 2
|
||||
bc = canvas.Pt(rect.x + rect.width - br * 0.35, rect.y + br * 0.35)
|
||||
canvas.disc_at(bc, br, _dim(canvas.BLACK, alpha))
|
||||
canvas.annulus(bc, br - 2, br, 0, 360, 24, _dim(_SL_DARK, alpha))
|
||||
canvas.glyphs_centered(self._bold, badge, 11 if len(badge) >= 3 else 13, bc, _dim(canvas.WHITE, alpha))
|
||||
|
||||
def _mutcd(self, rect, value, badge, tint, has_limit, alpha=1.0):
|
||||
canvas.panel(rect, 0.25, 8, _dim(canvas.WHITE, alpha))
|
||||
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))
|
||||
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))
|
||||
canvas.panel_outline(chip, 0.35, 8, 2, _dim(_SL_DARK, alpha))
|
||||
canvas.glyphs_centered(self._bold, badge, 11 if len(badge) >= 3 else 13,
|
||||
canvas.Pt(chip.x + _BADGE_SIDE / 2, chip.y + _BADGE_SIDE / 2), _dim(canvas.WHITE, alpha))
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
@@ -2,11 +2,11 @@
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.ui_state import UIStatus
|
||||
from iqpilot.selfdrive.ui.ui_state import UIStatus
|
||||
|
||||
BORDER_COLORS_IQ = {
|
||||
UIStatus.LAT_ONLY: rl.Color(0x0C, 0x94, 0x96, 0xFF),
|
||||
UIStatus.LONG_ONLY: rl.Color(0x96, 0x1C, 0xA8, 0xFF), # Purple for longitudinal-only state
|
||||
UIStatus.LONG_ONLY: rl.Color(0x96, 0x1C, 0xA8, 0xFF),
|
||||
}
|
||||
|
||||
|
||||
|
||||
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)
|
||||
@@ -5,22 +5,21 @@ import time
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui import UI_BORDER_SIZE
|
||||
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer, BTN_SIZE, ARC_LENGTH
|
||||
from openpilot.iqpilot.ui.onroad.hud_overlays import IQDevMetricsOverlay
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.selfdrive.ui import UI_BORDER_SIZE
|
||||
from iqpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer, BTN_SIZE, ARC_LENGTH
|
||||
from iqpilot.ui.onroad.hud_overlays import IQDevMetricsOverlay
|
||||
from iqpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from iqpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
|
||||
# LongitudinalPersonality ordinals (matches cereal enum: relaxed=0, standard=1, aggressive=2)
|
||||
_PERSONALITY_RELAXED = 0
|
||||
_PERSONALITY_STANDARD = 1
|
||||
_PERSONALITY_AGGRESSIVE = 2
|
||||
|
||||
PERSONALITY_COLORS = {
|
||||
_PERSONALITY_RELAXED: rl.Color(0x17, 0xC9, 0x64, 0xFF), # green
|
||||
_PERSONALITY_STANDARD: rl.Color(0x0C, 0x94, 0x96, 0xFF), # teal
|
||||
_PERSONALITY_AGGRESSIVE: rl.Color(0xE8, 0x2C, 0x2C, 0xFF), # red
|
||||
_PERSONALITY_RELAXED: rl.Color(0x17, 0xC9, 0x64, 0xFF),
|
||||
_PERSONALITY_STANDARD: rl.Color(0x0C, 0x94, 0x96, 0xFF),
|
||||
_PERSONALITY_AGGRESSIVE: rl.Color(0xE8, 0x2C, 0x2C, 0xFF),
|
||||
}
|
||||
|
||||
PERSONALITY_NAMES = {
|
||||
@@ -29,14 +28,13 @@ PERSONALITY_NAMES = {
|
||||
_PERSONALITY_AGGRESSIVE: "Aggressive",
|
||||
}
|
||||
|
||||
_TOAST_DURATION = 2.0 # seconds
|
||||
_TOAST_DURATION = 2.0
|
||||
_TOAST_FONT_SIZE = 52
|
||||
_TOAST_PAD_X = 52
|
||||
_TOAST_PAD_Y = 22
|
||||
_TOAST_BOTTOM_MARGIN = UI_BORDER_SIZE + 36
|
||||
_TOAST_RADIUS = 0.45
|
||||
_TOAST_FADE = 0.25 # fade-in / fade-out window
|
||||
|
||||
_TOAST_FADE = 0.25
|
||||
|
||||
class DriverStateRendererIQ(DriverStateRenderer):
|
||||
def __init__(self):
|
||||
@@ -135,7 +133,6 @@ class DriverStateRendererIQ(DriverStateRenderer):
|
||||
)
|
||||
|
||||
def _pre_calculate_drawing_elements(self):
|
||||
"""Pre-calculate all drawing elements based on the current rectangle"""
|
||||
width, height = self._rect.width, self._rect.height
|
||||
offset = UI_BORDER_SIZE + BTN_SIZE // 2
|
||||
self.position_x = self._rect.x + (width - offset if self.is_rhd else offset)
|
||||
|
||||
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)
|
||||
@@ -1,11 +1,5 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
|
||||
Consolidated IQ.Pilot onroad overlays. Every HUD widget that decorates the
|
||||
driving view — the accel strip, blind-spot flags, speed readout, road-name
|
||||
capsule, turn indicators, nav-provider badge and lead chevron labels — lives
|
||||
here and paints through the shared canvas facade. Grouping them keeps one
|
||||
import surface and one drawing vocabulary for the whole overlay layer.
|
||||
"""
|
||||
import math
|
||||
import time
|
||||
@@ -13,21 +7,19 @@ import time
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
|
||||
from cereal import car, custom
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.onroad.hud_renderer import COLORS, FONT_SIZES, UI_CONFIG
|
||||
from openpilot.selfdrive.ui.mici.onroad.alert_renderer import IconSide, TURN_SIGNAL_BLINK_PERIOD
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.iqwidgets.lib import canvas
|
||||
from iqpilot.cereal import car, custom
|
||||
from iqpilot.common.constants import CV
|
||||
from iqpilot.common.filter_simple import FirstOrderFilter
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.selfdrive.ui.onroad.hud_renderer import COLORS, FONT_SIZES, UI_CONFIG
|
||||
from iqpilot.selfdrive.ui.mici.onroad.alert_renderer import IconSide, TURN_SIGNAL_BLINK_PERIOD
|
||||
from iqpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar
|
||||
from iqpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
from iqpilot.system.ui.widgets import Widget
|
||||
from iqpilot.system.ui.iqwidgets.lib import canvas
|
||||
from iqdbc.car.volkswagen.values import VolkswagenFlags
|
||||
|
||||
|
||||
# --- shared state access -----------------------------------------------------
|
||||
|
||||
def _feed():
|
||||
return ui_state.sm
|
||||
|
||||
@@ -36,9 +28,6 @@ def _speed_scale() -> float:
|
||||
return CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Accel strip
|
||||
# ============================================================================
|
||||
_STRIP_WIDTH = 28
|
||||
_STRIP_INSET = 14
|
||||
_STRIP_CEILING = 0.85
|
||||
@@ -59,7 +48,7 @@ _STRIP_HALO_SPREAD = 11.0
|
||||
_STRIP_HALO_LAYERS = 11
|
||||
_STRIP_HALO_ALPHA = 0.095
|
||||
_STRIP_HALO_TAIL = 34.0
|
||||
_STRIP_NEON_FULL = 2.0 # m/s^2 at which the edges reach full brightness
|
||||
_STRIP_NEON_FULL = 2.0
|
||||
|
||||
|
||||
class IQAccelBar:
|
||||
@@ -139,10 +128,6 @@ class IQAccelBar:
|
||||
self._cap(canvas.Pt(x + cap, top + cap), cap, True, self._tint(fill, frac_top), neon, heat, tail)
|
||||
self._cap(canvas.Pt(x + cap, top + reach - cap), cap, False, self._tint(fill, frac_bottom), neon, heat, tail)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Blind-spot flags
|
||||
# ============================================================================
|
||||
_BS_INSET = 20
|
||||
_BS_DROP = 100
|
||||
_BS_MIN = 0.01
|
||||
@@ -187,13 +172,7 @@ class IQBlindSpotOverlay:
|
||||
if side.lit():
|
||||
side.place(rect)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Speed readout
|
||||
# ============================================================================
|
||||
_MQB_CLUSTER_EXEMPT = (VolkswagenFlags.PQ | VolkswagenFlags.MLB | VolkswagenFlags.MEB |
|
||||
VolkswagenFlags.MEB_GEN2 | VolkswagenFlags.MQB_EVO)
|
||||
|
||||
_MQB_CLUSTER_EXEMPT = (VolkswagenFlags.PQ | VolkswagenFlags.MLB | VolkswagenFlags.MEB | VolkswagenFlags.MEB_GEN2 | VolkswagenFlags.MQB_EVO)
|
||||
|
||||
class IQSpeedOverlay:
|
||||
def __init__(self):
|
||||
@@ -223,10 +202,6 @@ class IQSpeedOverlay:
|
||||
unit = tr("km/h") if ui_state.is_metric else tr("mph")
|
||||
self._stack(self._mid, unit, FONT_SIZES.speed_unit, rect, top + number_h - 10, COLORS.WHITE_TRANSLUCENT)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Road-name capsule
|
||||
# ============================================================================
|
||||
def clip_to_width(font, words: str, size: int, limit: float) -> str:
|
||||
if canvas.span(font, words, size).x <= limit:
|
||||
return words
|
||||
@@ -238,21 +213,14 @@ def clip_to_width(font, words: str, size: int, limit: float) -> str:
|
||||
|
||||
class RoadNameBanner(Widget):
|
||||
TYPE_SIZE = 46
|
||||
FLOOR_WIDTH = 200
|
||||
SIDE_PAD = 40
|
||||
MARGIN = 40
|
||||
DROP = -4
|
||||
BAR_H = 60
|
||||
CURVE = 0.2
|
||||
SEGS = 10
|
||||
BACKDROP_A = 120
|
||||
INK_A = 200
|
||||
INNER_PAD = 20
|
||||
GAP = 10
|
||||
TORQUE_SCALE = 3.0
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.road_name = ""
|
||||
self._face = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
self._face = gui_app.font(FontWeight.BOLD)
|
||||
|
||||
def update(self):
|
||||
sm = _feed()
|
||||
@@ -264,24 +232,18 @@ class RoadNameBanner(Widget):
|
||||
def _render(self, rect):
|
||||
if not self.road_name or not ui_state.road_name_toggle:
|
||||
return
|
||||
natural = canvas.span(self._face, self.road_name, self.TYPE_SIZE).x
|
||||
bar_w = max(self.FLOOR_WIDTH, min(natural + self.SIDE_PAD, rect.width - self.MARGIN))
|
||||
bar = canvas.Box(rect.x + (rect.width - bar_w) / 2, rect.y + self.DROP, bar_w, self.BAR_H)
|
||||
canvas.panel(bar, self.CURVE, self.SEGS, canvas.shade(0, 0, 0, self.BACKDROP_A))
|
||||
label = clip_to_width(self._face, self.road_name, self.TYPE_SIZE, bar.width - self.INNER_PAD)
|
||||
label = clip_to_width(self._face, self.road_name, self.TYPE_SIZE, rect.width - self.MARGIN)
|
||||
extent = canvas.span(self._face, label, self.TYPE_SIZE)
|
||||
top = min(TorqueBar.resting_bottom(rect, self.TORQUE_SCALE) + self.GAP,
|
||||
rect.y + rect.height - extent.y)
|
||||
canvas.glyphs(self._face, label,
|
||||
canvas.Pt(bar.x + (bar.width - extent.x) / 2, bar.y + (bar.height - extent.y) / 2),
|
||||
self.TYPE_SIZE, canvas.shade(255, 255, 255, self.INK_A))
|
||||
canvas.Pt(rect.x + (rect.width - extent.x) / 2, top),
|
||||
self.TYPE_SIZE, COLORS.WHITE)
|
||||
|
||||
|
||||
RoadNameRenderer = RoadNameBanner
|
||||
ellipsize = clip_to_width
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Turn indicators
|
||||
# ============================================================================
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
_ARROW = 'signal'
|
||||
@@ -296,7 +258,6 @@ class TurnSignalConfig:
|
||||
right_y: int = 190
|
||||
size: int = 150
|
||||
|
||||
|
||||
class _IndicatorLamp(Widget):
|
||||
def __init__(self, direction: IconSide):
|
||||
super().__init__()
|
||||
@@ -314,10 +275,6 @@ class _IndicatorLamp(Widget):
|
||||
self.mode = mode
|
||||
|
||||
def _pulse(self) -> int:
|
||||
# onroad/offroad run at different target_fps (set_target_fps only takes effect after this
|
||||
# widget is constructed at offroad startup), so re-derive dt each frame instead of trusting
|
||||
# the fps baked in at __init__ — otherwise the glow decays ~3x too slowly onroad and never
|
||||
# visibly dims before the next reset, reading as a static-on arrow instead of a blink.
|
||||
self._glow.dt = 1 / gui_app.target_fps
|
||||
self._glow.update_alpha(0.3)
|
||||
if time.monotonic() - self._epoch > TURN_SIGNAL_BLINK_PERIOD:
|
||||
@@ -395,17 +352,12 @@ class IQTurnSignalOverlay:
|
||||
def config(self, new_config: TurnSignalConfig):
|
||||
self._config = new_config
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Nav-influence provider badge
|
||||
# ============================================================================
|
||||
_PROVIDER_TAGS = {0: "", 1: "NAV", 2: "MBX", 3: "VIS", 4: "OSM"}
|
||||
_NAV_TEX_W, _NAV_TEX_H = 256, 128
|
||||
_NAV_BADGE_W = 160
|
||||
_NAV_FONT = 36
|
||||
_NAV_SHIFT = -260
|
||||
|
||||
|
||||
class NavInfluenceRenderer(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -459,10 +411,6 @@ class NavInfluenceRenderer(Widget):
|
||||
canvas.stamp_scaled(self._offscreen.texture, canvas.Box(0, 0, _NAV_TEX_W, -_NAV_TEX_H),
|
||||
canvas.Box(ax, ay, _NAV_TEX_W, _NAV_TEX_H), canvas.Pt(0, 0), 0, canvas.WHITE)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Lead chevron labels
|
||||
# ============================================================================
|
||||
class ChevronOptions:
|
||||
OFF = 0
|
||||
DISTANCE_ONLY = 1
|
||||
@@ -470,7 +418,6 @@ class ChevronOptions:
|
||||
TTC_ONLY = 3
|
||||
ALL = 4
|
||||
|
||||
|
||||
_CH_FONT = 40
|
||||
_CH_LINE = 50
|
||||
_CH_MARGIN = 20
|
||||
@@ -478,29 +425,24 @@ _CH_FADE_DOWN = 0.05
|
||||
_CH_FADE_UP = 0.1
|
||||
_CH_DEDUP = 3.0
|
||||
|
||||
|
||||
def _gap_label(d_rel: float, _v_abs: float) -> str:
|
||||
val = max(0.0, d_rel)
|
||||
return f"{val:.0f} m" if ui_state.is_metric else f"{val * 3.28084:.0f} ft"
|
||||
|
||||
|
||||
def _pace_label(_d_rel: float, v_abs: float) -> str:
|
||||
unit = "km/h" if ui_state.is_metric else "mph"
|
||||
return f"{max(0.0, v_abs * _speed_scale()):.0f} {unit}"
|
||||
|
||||
|
||||
def _ttc_label(d_rel: float, _v_abs: float, v_ego: float) -> str:
|
||||
ttc = (d_rel / v_ego) if (d_rel > 0 and v_ego > 0) else 0.0
|
||||
return f"{ttc:.1f} s" if 0 < ttc < 200 else "---"
|
||||
|
||||
|
||||
_CH_METRICS = (
|
||||
((ChevronOptions.DISTANCE_ONLY, ChevronOptions.ALL), lambda d, va, ve: _gap_label(d, va)),
|
||||
((ChevronOptions.SPEED_ONLY, ChevronOptions.ALL), lambda d, va, ve: _pace_label(d, va)),
|
||||
((ChevronOptions.TTC_ONLY, ChevronOptions.ALL), _ttc_label),
|
||||
)
|
||||
|
||||
|
||||
class ChevronMetrics:
|
||||
def __init__(self):
|
||||
self._alpha: float = 0.0
|
||||
@@ -552,8 +494,6 @@ class ChevronMetrics:
|
||||
|
||||
@staticmethod
|
||||
def _active_leads(radar_state, markers):
|
||||
"""Yield (lead, marker) for tracked leads with a projected marker, dropping a
|
||||
second lead that sits within the dedup band of the first."""
|
||||
tracked = []
|
||||
for lead, marker in zip((radar_state.leadOne, radar_state.leadTwo), markers, strict=False):
|
||||
if lead and lead.status and marker.center is not None:
|
||||
@@ -571,11 +511,6 @@ class ChevronMetrics:
|
||||
for lead, marker in self._active_leads(radar_state, lead_vehicles):
|
||||
self._one_lead(lead, marker, v_ego, rect)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Developer telemetry bar
|
||||
# ============================================================================
|
||||
|
||||
_TEAL = canvas.shade(0x0C, 0x94, 0x96, 0xFF)
|
||||
_AMBER = canvas.shade(255, 188, 0, 255)
|
||||
_GREEN = canvas.shade(0, 255, 0, 255)
|
||||
@@ -587,7 +522,6 @@ _ANGLE_TYPES = (car.CarParams.SteerControlType.angle, car.CarParams.SteerControl
|
||||
|
||||
@dataclass
|
||||
class Readout:
|
||||
"""One bar cell: 'TAG value unit', with each part pre-measured for layout."""
|
||||
tag: str
|
||||
value: str
|
||||
unit: str = ""
|
||||
@@ -609,7 +543,6 @@ class Readout:
|
||||
self.unit_w = canvas.span(font, self.unit_text, px, 0).x if self.unit else 0
|
||||
self.span = self.tag_w + self.value_w + self.unit_w
|
||||
|
||||
# kept for external callers that used the old field/method names
|
||||
@property
|
||||
def total_width(self):
|
||||
return self.span
|
||||
@@ -620,62 +553,47 @@ class Readout:
|
||||
|
||||
UiElement = Readout
|
||||
|
||||
|
||||
# --- grading -----------------------------------------------------------------
|
||||
|
||||
def _banded(magnitude, warn, crit, ok):
|
||||
if magnitude > crit:
|
||||
return canvas.RED
|
||||
return _AMBER if magnitude > warn else ok
|
||||
|
||||
|
||||
def _closing(v_rel):
|
||||
return _banded(-v_rel if v_rel < 0 else 0.0, 0.0, 4.4704, canvas.WHITE)
|
||||
|
||||
|
||||
def _following(d_rel):
|
||||
if d_rel < 5:
|
||||
return canvas.RED
|
||||
return _AMBER if d_rel < 15 else canvas.WHITE
|
||||
|
||||
|
||||
def _steer_tint(sm):
|
||||
if not sm['carControl'].latActive:
|
||||
return canvas.WHITE
|
||||
return _GREY if sm['carState'].steeringPressed else _TEAL
|
||||
|
||||
|
||||
def _angle_tint(sm, deg):
|
||||
floor = _steer_tint(sm) if sm['carControl'].latActive else canvas.WHITE
|
||||
return _banded(abs(deg), 90.0, 180.0, floor)
|
||||
|
||||
|
||||
def _yaw_offset(sm):
|
||||
return sm['liveParameters'].angleOffsetAverageDeg if sm.valid['liveParameters'] else 0.0
|
||||
|
||||
return sm['vehicleParameters'].angleOffsetAverageDeg if sm.valid['vehicleParameters'] else 0.0
|
||||
|
||||
def _bank(sm):
|
||||
return sm['liveParameters'].roll if sm.valid['liveParameters'] else 0.0
|
||||
|
||||
return sm['vehicleParameters'].roll if sm.valid['vehicleParameters'] else 0.0
|
||||
|
||||
def _units(is_metric):
|
||||
return (CV.MS_TO_KPH, "km/h") if is_metric else (CV.MS_TO_MPH, "mph")
|
||||
|
||||
|
||||
def _fix(sm):
|
||||
for svc in ('gpsLocationExternal', 'gpsLocation'):
|
||||
if sm.valid[svc]:
|
||||
return sm[svc], svc
|
||||
return None, None
|
||||
|
||||
|
||||
# --- probes (sm, is_metric) -> Readout ---------------------------------------
|
||||
|
||||
def steering_angle(sm, is_metric):
|
||||
deg = sm['carState'].steeringAngleDeg - _yaw_offset(sm)
|
||||
return Readout("R.S.", f"{deg:.1f}°", color=_angle_tint(sm, deg))
|
||||
|
||||
|
||||
def desired_steering_angle(sm, is_metric):
|
||||
live = sm['carControl'].latActive
|
||||
off = _yaw_offset(sm)
|
||||
@@ -688,7 +606,6 @@ def desired_steering_angle(sm, is_metric):
|
||||
tint = _banded(abs(seen), 90.0, 180.0, _TEAL) if live else canvas.WHITE
|
||||
return Readout("D.S.", f"{want:.1f}°" if live else "-", color=tint)
|
||||
|
||||
|
||||
def desired_steering_pid(sm, is_metric):
|
||||
live = sm['carControl'].latActive
|
||||
off = _yaw_offset(sm)
|
||||
@@ -697,33 +614,27 @@ def desired_steering_pid(sm, is_metric):
|
||||
tint = _banded(abs(seen), 90.0, 180.0, _TEAL) if live else canvas.WHITE
|
||||
return Readout("D.S.", f"{want:.1f}°" if live else "-", color=tint)
|
||||
|
||||
|
||||
def actual_lat_accel(sm, is_metric):
|
||||
a = sm['controlsState'].curvature * sm['carState'].vEgo ** 2 - _bank(sm) * _G
|
||||
return Readout("A.L.A.", f"{a:.2f}", "m/s^2", _steer_tint(sm))
|
||||
|
||||
|
||||
def desired_lat_accel(sm, is_metric):
|
||||
live = sm['carControl'].latActive
|
||||
a = sm['controlsState'].desiredCurvature * sm['carState'].vEgo ** 2 - _bank(sm) * _G
|
||||
return Readout("D.L.A.", f"{a:.2f}" if live else "-", "m/s^2", _steer_tint(sm))
|
||||
|
||||
|
||||
def a_ego(sm, is_metric):
|
||||
return Readout("L.ACC.", f"{sm['carState'].aEgo:.1f}", "m/s^2")
|
||||
|
||||
|
||||
def lead_distance(sm, is_metric):
|
||||
lead = sm['radarState'].leadOne
|
||||
return Readout("REL DIST", "-", "m") if not lead.status else Readout("REL DIST", f"{lead.dRel:.0f}", "m", _following(lead.dRel))
|
||||
|
||||
|
||||
def lead_rel_speed(sm, is_metric):
|
||||
lead = sm['radarState'].leadOne
|
||||
k, unit = _units(is_metric)
|
||||
return Readout("REL SPEED", "-", unit) if not lead.status else Readout("REL SPEED", f"{lead.vRel * k:.0f}", unit, _closing(lead.vRel))
|
||||
|
||||
|
||||
def lead_speed(sm, is_metric):
|
||||
lead = sm['radarState'].leadOne
|
||||
k, unit = _units(is_metric)
|
||||
@@ -731,24 +642,19 @@ def lead_speed(sm, is_metric):
|
||||
return Readout("L.S.", "-", unit)
|
||||
return Readout("L.S.", f"{(lead.vRel + sm['carState'].vEgo) * k:.0f}", unit, _closing(lead.vRel))
|
||||
|
||||
|
||||
def friction_coefficient(sm, is_metric):
|
||||
ltp = sm['liveTorqueParameters']
|
||||
return Readout("FRIC.", f"{ltp.frictionCoefficientFiltered:.3f}", color=_GREEN if ltp.liveValid else canvas.WHITE)
|
||||
|
||||
ltp = sm['lateralTorqueParameters']
|
||||
return Readout("FRIC.", f"{ltp.frictionCoefficientFiltered:.3f}", color=_GREEN if ltp.valid else canvas.WHITE)
|
||||
|
||||
def lat_accel_factor(sm, is_metric):
|
||||
ltp = sm['liveTorqueParameters']
|
||||
return Readout("L.A.F.", f"{ltp.latAccelFactorFiltered:.3f}", color=_GREEN if ltp.liveValid else canvas.WHITE)
|
||||
|
||||
ltp = sm['lateralTorqueParameters']
|
||||
return Readout("L.A.F.", f"{ltp.latAccelFactorFiltered:.3f}", color=_GREEN if ltp.valid else canvas.WHITE)
|
||||
|
||||
def eps_torque(sm, is_metric):
|
||||
return Readout("E.T.", f"{abs(sm['carState'].steeringTorqueEps):.1f}", "N·dm")
|
||||
|
||||
|
||||
_COMPASS = ("N", "NE", "E", "SE", "S", "SW", "W", "NW")
|
||||
|
||||
|
||||
def bearing(sm, is_metric):
|
||||
fix, _ = _fix(sm)
|
||||
if fix is None or fix.bearingAccuracyDeg == 180.0:
|
||||
@@ -756,7 +662,6 @@ def bearing(sm, is_metric):
|
||||
heading = _COMPASS[int(((fix.bearingDeg + 22.5) % 360) // 45)]
|
||||
return Readout("B.D.", f"{heading} | {fix.bearingDeg:.0f}°")
|
||||
|
||||
|
||||
def altitude(sm, is_metric):
|
||||
fix, svc = _fix(sm)
|
||||
if fix is None:
|
||||
@@ -764,9 +669,7 @@ def altitude(sm, is_metric):
|
||||
acc = fix.horizontalAccuracy if svc == 'gpsLocationExternal' else 1.0
|
||||
return Readout("ALT.", f"{fix.altitude:.1f}" if acc != 0.0 else "-", "m")
|
||||
|
||||
|
||||
def _desired_probe(sm):
|
||||
"""The 'desired' cell tracks whichever lateral controller is live."""
|
||||
if sm['controlsState'].lateralControlState.which() == 'angleState':
|
||||
return desired_steering_angle
|
||||
if ui_state.CP is not None and ui_state.CP.steerControlType in _ANGLE_TYPES:
|
||||
@@ -775,7 +678,6 @@ def _desired_probe(sm):
|
||||
return desired_steering_pid
|
||||
return desired_lat_accel
|
||||
|
||||
|
||||
class IQDevMetricsOverlay(Widget):
|
||||
DEV_UI_OFF = 0
|
||||
DEV_UI_RIGHT = 1
|
||||
@@ -830,10 +732,6 @@ class IQDevMetricsOverlay(Widget):
|
||||
if cell.unit:
|
||||
canvas.glyphs(self._face, cell.unit_text, canvas.Pt(x + cell.tag_w + cell.value_w, y), _BAR_FONT, canvas.WHITE)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Speed-limit sign (Vienna / MUTCD) + limit-ahead preview + assist arrows
|
||||
# ============================================================================
|
||||
_SL_M_TO_FT = 3.28084
|
||||
_SL_M_TO_MI = 0.000621371
|
||||
_SL_AHEAD_STEPS = 5
|
||||
@@ -844,14 +742,10 @@ _SL_DARK = canvas.shade(77, 77, 77, 255)
|
||||
_SL_PANEL_BG = canvas.shade(0, 0, 0, 180)
|
||||
_SL_PANEL_EDGE = canvas.shade(255, 255, 255, 100)
|
||||
|
||||
|
||||
def _dim(color, alpha: float):
|
||||
return canvas.with_opacity(color, 255 * alpha)
|
||||
|
||||
|
||||
class IQSpeedLimitOverlay(Widget):
|
||||
"""Regulatory sign, upcoming-limit preview and pre-active nudge arrows."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.speed_limit = 0.0
|
||||
@@ -879,8 +773,8 @@ class IQSpeedLimitOverlay(Widget):
|
||||
self._pulse_ema = FirstOrderFilter(1.0, 0.5, 1 / gui_app.target_fps)
|
||||
|
||||
px = 90
|
||||
self._up = gui_app.texture("../../iqpilot/selfdrive/assets/img_plus_arrow_up.png", px, px)
|
||||
self._down = gui_app.texture("../../iqpilot/selfdrive/assets/img_minus_arrow_down.png", px, px)
|
||||
self._up = gui_app.texture("img_plus_arrow_up.png", px, px)
|
||||
self._down = gui_app.texture("img_minus_arrow_down.png", px, px)
|
||||
|
||||
@property
|
||||
def speed_limit_assist_state(self):
|
||||
@@ -931,13 +825,13 @@ class IQSpeedLimitOverlay(Widget):
|
||||
badge = ""
|
||||
if self.speed_limit_offset != 0:
|
||||
badge = f"{'' if self.speed_limit_offset > 0 else '-'}{round(abs(self.speed_limit_offset))}"
|
||||
warn = ui_state.speed_limit_mode >= 2 # SpeedLimitMode.warning
|
||||
warn = ui_state.speed_limit_mode >= 2
|
||||
over = has_limit and round(self.speed_limit_final_last) < round(self.speed)
|
||||
tint = canvas.RED if (warn and over) else (_SL_GREY if not self.speed_limit_valid else canvas.BLACK)
|
||||
return value, badge, tint, has_limit
|
||||
|
||||
def _render(self, rect):
|
||||
if ui_state.speed_limit_mode == 0: # SpeedLimitMode.off
|
||||
if ui_state.speed_limit_mode == 0:
|
||||
return
|
||||
w = UI_CONFIG.set_speed_width_metric if ui_state.is_metric else UI_CONFIG.set_speed_width_imperial
|
||||
sign = canvas.Box(rect.x + 60 - 6, rect.y + 45 + UI_CONFIG.set_speed_height + 12, w + 12, 160)
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
|
||||
IQ.Pilot road-view HUD: extends the stock renderer and layers on the IQ overlays
|
||||
(developer bar, nav map, road name, speed + speed-limit, turn signals, rocket-fuel
|
||||
accel bar, soft warnings, steering arc).
|
||||
"""
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer
|
||||
from openpilot.iqpilot.ui.onroad.hud_overlays import (
|
||||
from iqpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer
|
||||
from iqpilot.ui.onroad.hud_overlays import (
|
||||
IQDevMetricsOverlay,
|
||||
RoadNameRenderer,
|
||||
IQAccelBar,
|
||||
@@ -18,8 +14,9 @@ from openpilot.iqpilot.ui.onroad.hud_overlays import (
|
||||
IQTurnSignalOverlay,
|
||||
IQSpeedOverlay,
|
||||
)
|
||||
from openpilot.iqpilot.ui.onroad.nav_map_panel import NavMapPanel
|
||||
from openpilot.iqpilot.ui.onroad.soft_warning import SoftWarningRenderer
|
||||
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
|
||||
@@ -29,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()
|
||||
@@ -42,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
|
||||
@@ -59,17 +58,18 @@ class IQHudRenderer(HudRenderer):
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
super()._render(rect)
|
||||
|
||||
torque_rect = rect
|
||||
if ui_state.developer_ui in (IQDevMetricsOverlay.DEV_UI_BOTTOM, IQDevMetricsOverlay.DEV_UI_BOTH):
|
||||
torque_rect = rl.Rectangle(rect.x, rect.y, rect.width, rect.height - IQDevMetricsOverlay.BOTTOM_BAR_HEIGHT)
|
||||
if ui_state.torque_bar:
|
||||
torque_rect = rect
|
||||
if ui_state.developer_ui in (IQDevMetricsOverlay.DEV_UI_BOTTOM, IQDevMetricsOverlay.DEV_UI_BOTH):
|
||||
torque_rect = rl.Rectangle(rect.x, rect.y, rect.width, rect.height - IQDevMetricsOverlay.BOTTOM_BAR_HEIGHT)
|
||||
self._torque_bar.render(torque_rect)
|
||||
|
||||
if not self.split_nav_enabled():
|
||||
self.developer_ui.render(rect)
|
||||
if ENABLE_FLOATING_NAV_MAP_PANEL:
|
||||
self.nav_map_panel.render(rect)
|
||||
self.road_name_renderer.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)
|
||||
self.rocket_fuel.render(rect, ui_state.sm)
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
import pyray as rl
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from iqpilot.common.filter_simple import FirstOrderFilter
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from iqpilot.system.ui.lib.application import gui_app
|
||||
|
||||
ACTIVE_TOP = rl.Color(0x22, 0xB8, 0xB9, 255)
|
||||
ACTIVE_BOTTOM = rl.Color(0x0C, 0x94, 0x96, 255)
|
||||
|
||||
@@ -13,12 +13,12 @@ except Exception:
|
||||
import pyray as rl
|
||||
import requests
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.iq_perf import PerfSample, PerfTraceEmitter
|
||||
from openpilot.common.params import Params, UnknownKeyName
|
||||
from openpilot.selfdrive.ui.lib.nav_helpers import current_or_last_gps_position, resolve_mapbox_token
|
||||
from openpilot.selfdrive.ui.lib.local_routes import utc_offset_hours
|
||||
from openpilot.iqpilot.ui.onroad.offline_tiles import (
|
||||
from iqpilot.common.basedir import BASEDIR
|
||||
from iqpilot.common.iq_perf import PerfSample, PerfTraceEmitter
|
||||
from iqpilot.common.params import Params, UnknownKeyName
|
||||
from iqpilot.selfdrive.ui.lib.nav_helpers import current_or_last_gps_position, resolve_mapbox_token
|
||||
from iqpilot.selfdrive.ui.lib.local_routes import utc_offset_hours
|
||||
from iqpilot.ui.onroad.offline_tiles import (
|
||||
find_offline_mbtiles_path,
|
||||
find_offline_xyz_root,
|
||||
load_raster_tile_blob,
|
||||
@@ -28,7 +28,7 @@ from openpilot.iqpilot.ui.onroad.offline_tiles import (
|
||||
open_mbtiles,
|
||||
xyz_zoom_bounds,
|
||||
)
|
||||
from openpilot.iqpilot.ui.onroad.nav_map_utils import (
|
||||
from iqpilot.ui.onroad.nav_map_utils import (
|
||||
build_mapbox_tile_url,
|
||||
choose_nav_camera,
|
||||
mercator_world_px_at_zoom,
|
||||
@@ -36,11 +36,11 @@ from openpilot.iqpilot.ui.onroad.nav_map_utils import (
|
||||
project_nav_polyline,
|
||||
solar_elevation_deg,
|
||||
)
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from iqpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from iqpilot.system.ui.lib.wrap_text import wrap_text
|
||||
from iqpilot.system.ui.widgets import Widget
|
||||
|
||||
PANEL_WIDTH = 560
|
||||
PANEL_HEIGHT = 600
|
||||
@@ -52,7 +52,7 @@ SPLIT_HEADER_HEIGHT = 160
|
||||
SPLIT_FOOTER_HEIGHT = 112
|
||||
# parents[4] pointed one level above the repo (stock selfdrive/assets has no nav icons),
|
||||
# so the maneuver arrow never loaded anywhere — anchor to BASEDIR instead
|
||||
ICON_ASSET_DIR = Path(BASEDIR) / "iqpilot" / "selfdrive" / "assets" / "navigation"
|
||||
ICON_ASSET_DIR = Path(BASEDIR) / "iqpilot" / "iqpilot" / "selfdrive" / "assets" / "navigation"
|
||||
STAT_GAP = 10
|
||||
TILE_SIZE = 256
|
||||
# env-overridable GPU-texture footprint levers; CACHE_LIMIT must stay >= the keep-set (~(visible + 2*margin)^2)
|
||||
|
||||
@@ -3,20 +3,14 @@ from urllib.parse import quote
|
||||
|
||||
EARTH_RADIUS_M = 6378137.0
|
||||
TILE_SIZE = 256.0
|
||||
|
||||
# Shift the whole driving zoom window closer in. The route-ahead fit (fit_zoom_for_points)
|
||||
# still zooms out for distant turns and in for straight roads; this just biases the baseline
|
||||
# so the route line + ego marker are easier to read while driving.
|
||||
NAV_DRIVE_ZOOM_BOOST = 1.0
|
||||
|
||||
|
||||
def _mercator_normalized(latitude: float, longitude: float) -> tuple[float, float]:
|
||||
x = (longitude + 180.0) / 360.0
|
||||
siny = min(max(math.sin(math.radians(latitude)), -0.9999), 0.9999)
|
||||
y = 0.5 - math.log((1.0 + siny) / (1.0 - siny)) / (4.0 * math.pi)
|
||||
return x, y
|
||||
|
||||
|
||||
def mercator_world_px(latitude: float, longitude: float, zoom: float) -> tuple[float, float]:
|
||||
world_size = TILE_SIZE * (2.0 ** zoom)
|
||||
nx, ny = _mercator_normalized(latitude, longitude)
|
||||
@@ -24,7 +18,6 @@ def mercator_world_px(latitude: float, longitude: float, zoom: float) -> tuple[f
|
||||
y = ny * world_size
|
||||
return x, y
|
||||
|
||||
|
||||
def destination_point(latitude: float, longitude: float, bearing_deg: float, distance_m: float) -> tuple[float, float]:
|
||||
if abs(distance_m) < 1e-3:
|
||||
return latitude, longitude
|
||||
@@ -46,7 +39,6 @@ def destination_point(latitude: float, longitude: float, bearing_deg: float, dis
|
||||
)
|
||||
return math.degrees(lat2), math.degrees(lon2)
|
||||
|
||||
|
||||
def fit_zoom_for_points(points, width: float, height: float, max_zoom: float = 17.6,
|
||||
min_zoom: float = 12.8, padding: float = 56.0) -> float:
|
||||
coords = [(float(point.latitude), float(point.longitude)) for point in points if point is not None]
|
||||
@@ -63,7 +55,6 @@ def fit_zoom_for_points(points, width: float, height: float, max_zoom: float = 1
|
||||
zoom_y = math.log2(usable_height / (TILE_SIZE * span_y))
|
||||
return max(min(min(zoom_x, zoom_y), max_zoom), min_zoom)
|
||||
|
||||
|
||||
def choose_nav_camera(current_latitude: float, current_longitude: float, bearing_deg: float, points,
|
||||
width: float, height: float, preferred_zoom: float) -> tuple[float, float, float]:
|
||||
preferred_zoom += NAV_DRIVE_ZOOM_BOOST
|
||||
@@ -78,19 +69,16 @@ def choose_nav_camera(current_latitude: float, current_longitude: float, bearing
|
||||
center_latitude, center_longitude = destination_point(current_latitude, current_longitude, bearing_deg, lookahead_m)
|
||||
return center_latitude, center_longitude, zoom
|
||||
|
||||
|
||||
def build_mapbox_static_url(latitude: float, longitude: float, zoom: float, bearing: float,
|
||||
width: int, height: int, points=None) -> str:
|
||||
overlay = ""
|
||||
if points:
|
||||
overlay = f"path-7+34d17a-0.85({encode_polyline(points)})/"
|
||||
|
||||
return (
|
||||
f"https://api.mapbox.com/styles/v1/mapbox/navigation-night-v1/static/"
|
||||
f"{overlay}{longitude:.6f},{latitude:.6f},{zoom:.2f},{bearing:.1f},0/{width}x{height}@2x"
|
||||
)
|
||||
|
||||
|
||||
def build_mapbox_tile_url(z: int, x: int, y: int, tile_size: int = 256, scale: int = 2,
|
||||
style: str = "navigation-night-v1") -> str:
|
||||
suffix = f"@{scale}x" if scale > 1 else ""
|
||||
@@ -99,17 +87,14 @@ def build_mapbox_tile_url(z: int, x: int, y: int, tile_size: int = 256, scale: i
|
||||
f"{tile_size}/{z}/{x}/{y}{suffix}"
|
||||
)
|
||||
|
||||
|
||||
def tile_world_size(z: int, tile_size: int = 256) -> int:
|
||||
return tile_size * (2 ** z)
|
||||
|
||||
|
||||
def mercator_world_px_at_zoom(latitude: float, longitude: float, z: int, tile_size: int = 256) -> tuple[float, float]:
|
||||
world_size = tile_world_size(z, tile_size)
|
||||
nx, ny = _mercator_normalized(latitude, longitude)
|
||||
return nx * world_size, ny * world_size
|
||||
|
||||
|
||||
def encode_polyline(points) -> str:
|
||||
result = []
|
||||
last_lat = 0
|
||||
@@ -131,7 +116,6 @@ def encode_polyline(points) -> str:
|
||||
|
||||
return quote("".join(result), safe="")
|
||||
|
||||
|
||||
def project_nav_point(latitude: float, longitude: float, center_latitude: float, center_longitude: float,
|
||||
zoom: float, bearing_deg: float, width: float, height: float,
|
||||
anchor_x: float = 0.5, anchor_y: float = 0.5) -> tuple[float, float]:
|
||||
@@ -147,7 +131,6 @@ def project_nav_point(latitude: float, longitude: float, center_latitude: float,
|
||||
ry = -dx * sin_theta + dy * cos_theta
|
||||
return width * anchor_x + rx, height * anchor_y + ry
|
||||
|
||||
|
||||
def project_nav_polyline(points, center_latitude: float, center_longitude: float, zoom: float, bearing_deg: float,
|
||||
width: float, height: float, anchor_x: float = 0.5, anchor_y: float = 0.5) -> list[tuple[float, float]]:
|
||||
projected = []
|
||||
@@ -168,10 +151,8 @@ def project_nav_polyline(points, center_latitude: float, center_longitude: float
|
||||
)
|
||||
return projected
|
||||
|
||||
|
||||
def solar_elevation_deg(latitude: float, longitude: float, unix_time: float) -> float:
|
||||
"""Approximate solar elevation (NOAA-style, good to ~0.5 deg) for day/night map styling."""
|
||||
days = unix_time / 86400.0 - 10957.5 # days since J2000 epoch
|
||||
days = unix_time / 86400.0 - 10957.5
|
||||
mean_longitude = math.radians((280.460 + 0.9856474 * days) % 360.0)
|
||||
mean_anomaly = math.radians((357.528 + 0.9856003 * days) % 360.0)
|
||||
ecliptic_longitude = mean_longitude + math.radians(1.915) * math.sin(mean_anomaly) \
|
||||
|
||||
@@ -7,8 +7,7 @@ from pathlib import Path
|
||||
try:
|
||||
import sqlite3
|
||||
except Exception:
|
||||
sqlite3 = None # type: ignore[assignment]
|
||||
|
||||
sqlite3 = None
|
||||
|
||||
OFFLINE_MBTILES_ENV = "IQPILOT_OFFLINE_MBTILES"
|
||||
OFFLINE_TILE_ROOT_ENV = "IQPILOT_OFFLINE_TILE_ROOT"
|
||||
@@ -17,12 +16,10 @@ DEFAULT_OFFLINE_MAP_ROOT = Path("/data/offline_maps" if Path("/data").exists() e
|
||||
SQLITE_ERRORS = (sqlite3.Error,) if sqlite3 is not None else (Exception,)
|
||||
SQLiteConnection = Any
|
||||
|
||||
|
||||
def offline_tile_root() -> Path:
|
||||
override = os.getenv(OFFLINE_TILE_ROOT_ENV)
|
||||
return Path(override) if override else DEFAULT_OFFLINE_TILE_ROOT
|
||||
|
||||
|
||||
def offline_map_root() -> Path:
|
||||
root = offline_tile_root()
|
||||
if root.name == "tiles":
|
||||
@@ -31,7 +28,6 @@ def offline_map_root() -> Path:
|
||||
return root.parent.parent if root.parent.name == "tiles" else root.parent
|
||||
return DEFAULT_OFFLINE_MAP_ROOT
|
||||
|
||||
|
||||
def _parse_bounds(bounds: str) -> tuple[float, float, float, float] | None:
|
||||
try:
|
||||
min_lon, min_lat, max_lon, max_lat = [float(part) for part in bounds.split(",")]
|
||||
@@ -39,22 +35,16 @@ def _parse_bounds(bounds: str) -> tuple[float, float, float, float] | None:
|
||||
return None
|
||||
return min_lat, min_lon, max_lat, max_lon
|
||||
|
||||
|
||||
def _bounds_contains(bounds: tuple[float, float, float, float], latitude: float, longitude: float) -> bool:
|
||||
min_lat, min_lon, max_lat, max_lon = bounds
|
||||
return min_lat <= latitude <= max_lat and min_lon <= longitude <= max_lon
|
||||
|
||||
|
||||
def _bounds_area(bounds: tuple[float, float, float, float]) -> float:
|
||||
min_lat, min_lon, max_lat, max_lon = bounds
|
||||
return max(max_lat - min_lat, 0.0) * max(max_lon - min_lon, 0.0)
|
||||
|
||||
|
||||
# Manual cache that only stores hits: caching a None (manifest not written yet — e.g. a
|
||||
# bundle download in flight) would otherwise pin the miss for the life of the process.
|
||||
_region_bounds_cache: dict[Path, tuple[float, float, float, float]] = {}
|
||||
|
||||
|
||||
def _load_region_bounds(region_root: Path) -> tuple[float, float, float, float] | None:
|
||||
cached = _region_bounds_cache.get(region_root)
|
||||
if cached is not None:
|
||||
@@ -66,7 +56,6 @@ def _load_region_bounds(region_root: Path) -> tuple[float, float, float, float]
|
||||
_region_bounds_cache[region_root] = bounds
|
||||
return bounds
|
||||
|
||||
|
||||
def _load_region_bounds_uncached(region_root: Path) -> tuple[float, float, float, float] | None:
|
||||
manifest_path = region_root / "manifest.json"
|
||||
if manifest_path.exists():
|
||||
@@ -92,13 +81,9 @@ def _load_region_bounds_uncached(region_root: Path) -> tuple[float, float, float
|
||||
|
||||
return _parse_bounds(row["value"]) if row is not None else None
|
||||
|
||||
|
||||
# Short-TTL cache instead of lru_cache: region bundles can be downloaded while the UI is
|
||||
# running, and a forever-cached candidate list would hide them until the process restarts.
|
||||
_REGION_ROOTS_TTL_S = 15.0
|
||||
_region_roots_cache: tuple[float, Path, tuple[Path, ...]] | None = None
|
||||
|
||||
|
||||
def _candidate_region_roots() -> tuple[Path, ...]:
|
||||
global _region_roots_cache
|
||||
root = offline_map_root()
|
||||
@@ -122,11 +107,10 @@ def _candidate_region_roots() -> tuple[Path, ...]:
|
||||
_region_roots_cache = (now, root, result)
|
||||
return result
|
||||
|
||||
|
||||
def _region_covers_point(region_root: Path, latitude: float, longitude: float) -> bool:
|
||||
mb = region_root / "tiles" / "offline.mbtiles"
|
||||
if not mb.exists():
|
||||
return True # xyz-only / unknown layout: don't second-guess the bbox match
|
||||
return True
|
||||
try:
|
||||
conn = open_mbtiles(mb)
|
||||
try:
|
||||
@@ -137,8 +121,6 @@ def _region_covers_point(region_root: Path, latitude: float, longitude: float) -
|
||||
lat_r = math.radians(max(min(latitude, 85.05112878), -85.05112878))
|
||||
x = int((longitude + 180.0) / 360.0 * n)
|
||||
y = int((1.0 - math.asinh(math.tan(lat_r)) / math.pi) / 2.0 * n)
|
||||
# 3x3 cluster: tolerate an empty sub-tile at the exact point (a z15 child with no road)
|
||||
# while still rejecting a neighbor whose coverage doesn't reach this area at all.
|
||||
for dx in (-1, 0, 1):
|
||||
for dy in (-1, 0, 1):
|
||||
if load_raster_tile_blob(conn, z, x + dx, y + dy) is not None:
|
||||
@@ -149,7 +131,6 @@ def _region_covers_point(region_root: Path, latitude: float, longitude: float) -
|
||||
except SQLITE_ERRORS:
|
||||
return True
|
||||
|
||||
|
||||
def find_offline_region_root(latitude: float | None = None, longitude: float | None = None) -> Path | None:
|
||||
candidates = _candidate_region_roots()
|
||||
if not candidates:
|
||||
@@ -169,8 +150,6 @@ def find_offline_region_root(latitude: float | None = None, longitude: float | N
|
||||
if bounded:
|
||||
if len(bounded) == 1:
|
||||
return bounded[0][1]
|
||||
# multiple bboxes overlap this point (border zone): prefer the smallest-area region that
|
||||
# ACTUALLY has tiles here, so we don't pick a neighbor whose bundle is empty at the border.
|
||||
bounded.sort(key=lambda item: item[0])
|
||||
for _, candidate in bounded:
|
||||
if _region_covers_point(candidate, latitude, longitude):
|
||||
@@ -179,7 +158,6 @@ def find_offline_region_root(latitude: float | None = None, longitude: float | N
|
||||
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def find_offline_mbtiles_path(latitude: float | None = None, longitude: float | None = None,
|
||||
day: bool = False) -> Path | None:
|
||||
explicit = os.getenv(OFFLINE_MBTILES_ENV)
|
||||
@@ -195,7 +173,6 @@ def find_offline_mbtiles_path(latitude: float | None = None, longitude: float |
|
||||
if region_root is None:
|
||||
return None
|
||||
|
||||
# day variant is optional: regions built before the day palette fall back to the night set
|
||||
if day:
|
||||
day_preferred = region_root / "tiles" / "offline_day.mbtiles"
|
||||
if day_preferred.exists():
|
||||
@@ -208,7 +185,6 @@ def find_offline_mbtiles_path(latitude: float | None = None, longitude: float |
|
||||
matches = sorted(p for p in (region_root / "tiles").glob("*.mbtiles") if "_day" not in p.name or day)
|
||||
return matches[0] if matches else None
|
||||
|
||||
|
||||
def find_offline_xyz_root(latitude: float | None = None, longitude: float | None = None) -> Path | None:
|
||||
root = offline_tile_root()
|
||||
if not root.exists():
|
||||
@@ -226,11 +202,9 @@ def find_offline_xyz_root(latitude: float | None = None, longitude: float | None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def xyz_to_tms_y(z: int, y: int) -> int:
|
||||
return (2 ** z - 1) - y
|
||||
|
||||
|
||||
def open_mbtiles(path: Path) -> SQLiteConnection:
|
||||
if sqlite3 is None:
|
||||
raise RuntimeError("sqlite3 unavailable")
|
||||
@@ -238,14 +212,12 @@ def open_mbtiles(path: Path) -> SQLiteConnection:
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
def mbtiles_is_raster(conn: SQLiteConnection) -> bool:
|
||||
row = conn.execute("SELECT value FROM metadata WHERE name = 'format'").fetchone()
|
||||
if row is None:
|
||||
return False
|
||||
return row["value"] in {"png", "jpg", "jpeg", "webp"}
|
||||
|
||||
|
||||
def mbtiles_zoom_bounds(conn: SQLiteConnection) -> tuple[int | None, int | None]:
|
||||
rows = {
|
||||
row["name"]: row["value"]
|
||||
@@ -255,7 +227,6 @@ def mbtiles_zoom_bounds(conn: SQLiteConnection) -> tuple[int | None, int | None]
|
||||
max_zoom = int(rows["maxzoom"]) if "maxzoom" in rows else None
|
||||
return min_zoom, max_zoom
|
||||
|
||||
|
||||
def xyz_zoom_bounds(root: Path) -> tuple[int | None, int | None]:
|
||||
zoom_dirs = sorted(
|
||||
int(child.name)
|
||||
@@ -266,7 +237,6 @@ def xyz_zoom_bounds(root: Path) -> tuple[int | None, int | None]:
|
||||
return None, None
|
||||
return zoom_dirs[0], zoom_dirs[-1]
|
||||
|
||||
|
||||
def load_raster_tile_blob(conn: SQLiteConnection, z: int, x: int, y: int) -> bytes | None:
|
||||
row = conn.execute(
|
||||
"""
|
||||
@@ -278,7 +248,6 @@ def load_raster_tile_blob(conn: SQLiteConnection, z: int, x: int, y: int) -> byt
|
||||
).fetchone()
|
||||
return bytes(row["tile_data"]) if row is not None else None
|
||||
|
||||
|
||||
def load_raster_xyz_tile_blob(root: Path, z: int, x: int, y: int) -> bytes | None:
|
||||
for suffix in ("png", "webp", "jpg", "jpeg"):
|
||||
for filename in (f"{y}.{suffix}", f"{y}@2x.{suffix}"):
|
||||
|
||||
@@ -2,32 +2,26 @@
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
import pyray as rl
|
||||
from cereal import log
|
||||
from iqpilot.cereal import log
|
||||
|
||||
from openpilot.selfdrive.ui import UI_BORDER_SIZE
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.onroad.driver_state import BTN_SIZE
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from iqpilot.selfdrive.ui import UI_BORDER_SIZE
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.selfdrive.ui.onroad.driver_state import BTN_SIZE
|
||||
from iqpilot.system.ui.lib.application import gui_app
|
||||
|
||||
EventName = log.OnroadEvent.EventName
|
||||
|
||||
# Events that trigger the soft warning triangle instead of a disruptive alert
|
||||
SOFT_WARNING_EVENTS = {
|
||||
EventName.commIssue,
|
||||
EventName.commIssueAvgFreq,
|
||||
EventName.selfdrivedLagging,
|
||||
}
|
||||
|
||||
ICON_SIZE = 96
|
||||
|
||||
# Speed box geometry (mirrors hud_renderer._draw_set_speed)
|
||||
_SPEED_BOX_X_OFFSET = 60
|
||||
_SPEED_BOX_Y_OFFSET = 45
|
||||
_SPEED_BOX_WIDTH = 180 # midpoint between metric (186) and imperial (174)
|
||||
_SPEED_BOX_WIDTH = 180
|
||||
_SPEED_BOX_HEIGHT = 228
|
||||
|
||||
_DM_OFFSET = UI_BORDER_SIZE + BTN_SIZE // 2 # = 126
|
||||
|
||||
_DM_OFFSET = UI_BORDER_SIZE + BTN_SIZE // 2
|
||||
|
||||
class SoftWarningRenderer:
|
||||
def __init__(self):
|
||||
@@ -41,16 +35,12 @@ class SoftWarningRenderer:
|
||||
def render(self, rect: rl.Rectangle) -> None:
|
||||
if not self._active:
|
||||
return
|
||||
|
||||
# Centre of speed box (top-left of screen)
|
||||
speed_cx = rect.x + _SPEED_BOX_X_OFFSET + _SPEED_BOX_WIDTH / 2
|
||||
speed_cy = rect.y + _SPEED_BOX_Y_OFFSET + _SPEED_BOX_HEIGHT / 2
|
||||
|
||||
# Centre of driver monitoring icon (bottom-left of screen, LHD)
|
||||
dm_cx = rect.x + _DM_OFFSET
|
||||
dm_cy = rect.y + rect.height - _DM_OFFSET
|
||||
|
||||
# Midpoint between the two
|
||||
mid_x = (speed_cx + dm_cx) / 2
|
||||
mid_y = (speed_cy + dm_cy) / 2
|
||||
|
||||
|
||||
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
|
||||
@@ -1,46 +1,21 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
|
||||
IQ.Pilot UI Theme — NeonTheme singleton
|
||||
========================================
|
||||
Stores the active accent/neon color as a hex string in Params["UIAccentColor"].
|
||||
This allows:
|
||||
- hephaestusd to write the color via BLE/RPC from the Konn3kt companion app
|
||||
- Konn3kt-set themes to be reflected on-device in real time
|
||||
- Future per-driver theme profiles via Konn3kt
|
||||
|
||||
Default accent: #00FFF5 (cyan neon — the IQ.Pilot signature color)
|
||||
|
||||
Any UI component that wants the accent color calls:
|
||||
NeonTheme.glow() -> rl.Color (full brightness, inner ring)
|
||||
NeonTheme.glow_mid() -> rl.Color (60% alpha, mid ring)
|
||||
NeonTheme.glow_outer() -> rl.Color (25% alpha, soft halo)
|
||||
NeonTheme.bg() -> rl.Color (darkened tint of accent for card bg)
|
||||
|
||||
Konn3kt / hephaestusd write interface:
|
||||
Params().put("UIAccentColor", "#00FFF5") # any CSS hex string
|
||||
|
||||
The theme refreshes from Params every REFRESH_INTERVAL seconds so changes
|
||||
propagate without a restart.
|
||||
"""
|
||||
|
||||
import time
|
||||
import pyray as rl
|
||||
|
||||
try:
|
||||
from openpilot.common.params import Params
|
||||
from iqpilot.common.params import Params
|
||||
except ImportError:
|
||||
Params = None
|
||||
|
||||
# How often (seconds) to re-read the accent color from Params
|
||||
REFRESH_INTERVAL = 2.0
|
||||
|
||||
# IQ.Pilot default signature neon cyan
|
||||
DEFAULT_ACCENT_HEX = "#00FFF5"
|
||||
|
||||
|
||||
def _hex_to_rgb(hex_str: str) -> tuple[int, int, int]:
|
||||
"""Parse a CSS hex color string like '#00FFF5' or '00FFF5' to (r, g, b)."""
|
||||
h = hex_str.strip().lstrip("#")
|
||||
if len(h) == 3:
|
||||
h = h[0] * 2 + h[1] * 2 + h[2] * 2
|
||||
@@ -53,16 +28,10 @@ def _hex_to_rgb(hex_str: str) -> tuple[int, int, int]:
|
||||
|
||||
|
||||
def _darken(r: int, g: int, b: int, factor: float = 0.08) -> tuple[int, int, int]:
|
||||
"""Mix the accent toward black to create a card bg tint."""
|
||||
return (int(r * factor), int(g * factor), int(b * factor))
|
||||
|
||||
|
||||
class _NeonTheme:
|
||||
"""
|
||||
Singleton that holds and lazily refreshes the active neon accent color.
|
||||
All color properties are rl.Color objects ready for use in Raylib draw calls.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._params = Params() if Params else None
|
||||
self._last_refresh: float = 0.0
|
||||
@@ -70,10 +39,6 @@ class _NeonTheme:
|
||||
self._r, self._g, self._b = _hex_to_rgb(DEFAULT_ACCENT_HEX)
|
||||
self._refresh()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _refresh(self):
|
||||
self._last_refresh = time.monotonic()
|
||||
if self._params is None:
|
||||
@@ -84,43 +49,33 @@ class _NeonTheme:
|
||||
self._hex = stored.strip()
|
||||
self._r, self._g, self._b = _hex_to_rgb(self._hex)
|
||||
except Exception:
|
||||
pass # keep previous value on any error
|
||||
pass
|
||||
|
||||
def _maybe_refresh(self):
|
||||
if time.monotonic() - self._last_refresh >= REFRESH_INTERVAL:
|
||||
self._refresh()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public color accessors
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def glow(self, alpha: int = 255) -> rl.Color:
|
||||
"""Full-brightness inner glow / border color."""
|
||||
self._maybe_refresh()
|
||||
return rl.Color(self._r, self._g, self._b, alpha)
|
||||
|
||||
def glow_mid(self, alpha: int = 130) -> rl.Color:
|
||||
"""Mid-brightness second ring."""
|
||||
self._maybe_refresh()
|
||||
return rl.Color(self._r, self._g, self._b, alpha)
|
||||
|
||||
def glow_outer(self, alpha: int = 45) -> rl.Color:
|
||||
"""Soft outer halo."""
|
||||
self._maybe_refresh()
|
||||
return rl.Color(self._r, self._g, self._b, alpha)
|
||||
|
||||
def bg(self) -> rl.Color:
|
||||
"""Dark card background tinted with the accent color."""
|
||||
self._maybe_refresh()
|
||||
dr, dg, db = _darken(self._r, self._g, self._b, 0.08)
|
||||
# minimum darkness so the card is always readable
|
||||
dr = max(dr, 0x0A)
|
||||
dg = max(dg, 0x0A)
|
||||
db = max(db, 0x0A)
|
||||
return rl.Color(dr, dg, db, 255)
|
||||
|
||||
def bg_pressed(self) -> rl.Color:
|
||||
"""Slightly lighter card background for press state."""
|
||||
self._maybe_refresh()
|
||||
dr, dg, db = _darken(self._r, self._g, self._b, 0.13)
|
||||
dr = max(dr, 0x0D)
|
||||
@@ -130,22 +85,10 @@ class _NeonTheme:
|
||||
|
||||
@property
|
||||
def hex(self) -> str:
|
||||
"""Current accent color as hex string, e.g. '#00FFF5'."""
|
||||
self._maybe_refresh()
|
||||
return self._hex
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Write interface (called by hephaestusd / Konn3kt theme handler)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def set_accent(self, hex_color: str):
|
||||
"""
|
||||
Set a new accent color immediately and persist it to Params.
|
||||
hephaestusd / Konn3kt companion app should call this.
|
||||
|
||||
Args:
|
||||
hex_color: CSS hex string, e.g. '#FF6B00' or '8B5CF6'
|
||||
"""
|
||||
r, g, b = _hex_to_rgb(hex_color)
|
||||
self._r, self._g, self._b = r, g, b
|
||||
self._hex = "#" + hex_color.strip().lstrip("#").upper()
|
||||
@@ -156,6 +99,4 @@ class _NeonTheme:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# Global singleton — import this everywhere
|
||||
NeonTheme = _NeonTheme()
|
||||
|
||||
Reference in New Issue
Block a user