IQ.Pilot Release Commit @ ba009d5
This commit is contained in:
@@ -369,6 +369,11 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
// never meant to survive a manager restart or reach a log.
|
||||
{"IQAndroidNavEmail", {CLEAR_ON_MANAGER_START | DONT_LOG, STRING}},
|
||||
{"IQAndroidNavPassword", {CLEAR_ON_MANAGER_START | DONT_LOG, STRING}},
|
||||
// Report-request flags: the UI writes 1 to ask navassist to file that report in the nav
|
||||
// app; navassist sets it back to 0 once the app confirms it was sent, so 0 == done.
|
||||
{"IQNavReportPolice", {PERSISTENT, BOOL, "0"}},
|
||||
{"IQNavReportCrash", {PERSISTENT, BOOL, "0"}},
|
||||
{"IQNavReportHazard", {PERSISTENT, BOOL, "0"}},
|
||||
{"IQLiveSteerDelay", {PERSISTENT, BOOL, "1"}},
|
||||
{"IQLateralAccelSlew", {PERSISTENT, BOOL, "0"}},
|
||||
{"IQLateralCurvatureLookahead", {PERSISTENT, BOOL, "0"}},
|
||||
|
||||
@@ -513,6 +513,9 @@ class InferenceDaemon:
|
||||
self._lat_smooth_extra_sec = 0.0
|
||||
|
||||
def _load_car_params(self, demo: bool):
|
||||
if not demo and self._params.get_bool("IQBenchIgnition") and self._params.get("CarParams") is None:
|
||||
cloudlog.warning("iqmodeld: bench ignition with no CarParams; running with the demo car")
|
||||
demo = True
|
||||
car_params = get_demo_car_params() if demo else messaging.log_from_bytes(
|
||||
self._params.get("CarParams", block=True), car.CarParams)
|
||||
cloudlog.info("iqmodeld got CarParams: %s", car_params.brand)
|
||||
|
||||
@@ -87,8 +87,6 @@ def main() -> None:
|
||||
print(f"saved warp JIT to {out} ({os.path.getsize(out) / 1e6:.2f} MB)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
def selftest_inputs(cam_w: int, cam_h: int, nv12_size: int):
|
||||
@@ -117,3 +115,7 @@ def selftest_digest(compiled, cam_w: int, cam_h: int, nv12_size: int) -> str:
|
||||
frame=Tensor(frame, device=dev).realize(),
|
||||
big_frame=Tensor(big_frame, device=dev).realize())
|
||||
return hashlib.sha256(out.numpy().astype(np.uint8).tobytes()).hexdigest()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -552,7 +552,8 @@ class ModelsLayoutMici(NavScroller):
|
||||
def _update_steer_delay_subtext(self):
|
||||
if self._steer_delay._checked:
|
||||
try:
|
||||
self._steer_delay.set_value(f"measured {ui_state.sm['lateralDelay'].lateralDelay:.3f} s")
|
||||
measured = ui_state.measured_steer_delay()
|
||||
self._steer_delay.set_value("calibrating" if measured is None else f"measured {measured:.3f} s")
|
||||
except Exception:
|
||||
self._steer_delay.set_value("")
|
||||
return
|
||||
|
||||
@@ -54,7 +54,9 @@ MICI_BORDER_THICKNESS = 50
|
||||
MICI_BORDER_ROUNDNESS = 0.2 * 1.02
|
||||
MICI_BORDER_BOTTOM_ONLY_HEIGHT = 95
|
||||
MICI_EXPERIMENTAL_ICON_SIZE = 28
|
||||
MICI_EXPERIMENTAL_ICON_SPACING = 8
|
||||
MICI_EXPERIMENTAL_ICON_SLOT = 60
|
||||
MICI_EXPERIMENTAL_ICON_MARGIN_X = 16
|
||||
MICI_EXPERIMENTAL_ICON_MARGIN_Y = 10
|
||||
|
||||
|
||||
class BookmarkIcon(Widget):
|
||||
@@ -274,7 +276,7 @@ class AugmentedRoadView(CameraView):
|
||||
# don't draw the experimental/IQ.Dynamic icon over alert text (it falls back to the
|
||||
# top-left alert anchor when the DMoji is hidden while disengaged)
|
||||
if alert_to_render is None:
|
||||
self._draw_experimental_icon(should_draw_dmoji)
|
||||
self._draw_experimental_icon()
|
||||
|
||||
# End clipping region
|
||||
rl.end_scissor_mode()
|
||||
@@ -304,7 +306,7 @@ class AugmentedRoadView(CameraView):
|
||||
rl.draw_rectangle(int(self.rect.x), int(self.rect.y), int(self.rect.width), int(self.rect.height), rl.Color(0, 0, 0, 175))
|
||||
self._offroad_label.render(self._content_rect)
|
||||
|
||||
def _draw_experimental_icon(self, draw_below_driver_state: bool) -> None:
|
||||
def _draw_experimental_icon(self) -> None:
|
||||
if not ui_state.started:
|
||||
return
|
||||
|
||||
@@ -316,13 +318,10 @@ class AugmentedRoadView(CameraView):
|
||||
else:
|
||||
icon = self._iqstandard_txt
|
||||
|
||||
if draw_below_driver_state:
|
||||
pos_x = self._rect.x + 16 + (self._driver_state_renderer.rect.width - icon.width) / 2
|
||||
pos_y = self._rect.y + 10 + self._driver_state_renderer.rect.height + MICI_EXPERIMENTAL_ICON_SPACING
|
||||
else:
|
||||
pos_x = self._rect.x + 18
|
||||
pos_y = self._rect.y + 18
|
||||
|
||||
slot_x = self._content_rect.x + self._content_rect.width - MICI_EXPERIMENTAL_ICON_MARGIN_X - MICI_EXPERIMENTAL_ICON_SLOT
|
||||
slot_y = self._content_rect.y + MICI_EXPERIMENTAL_ICON_MARGIN_Y
|
||||
pos_x = slot_x + (MICI_EXPERIMENTAL_ICON_SLOT - icon.width) / 2
|
||||
pos_y = slot_y + (MICI_EXPERIMENTAL_ICON_SLOT - icon.height) / 2
|
||||
rl.draw_texture(icon, int(pos_x), int(pos_y), rl.WHITE)
|
||||
|
||||
def _draw_border(self):
|
||||
|
||||
@@ -14,6 +14,7 @@ from iqpilot.selfdrive.ui.mici.onroad import blend_colors
|
||||
from iqpilot.system.ui.lib.application import gui_app
|
||||
from iqpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
|
||||
from iqpilot.system.ui.widgets import Widget
|
||||
from iqpilot.ui.theme import NeonTheme
|
||||
|
||||
CLIP_MARGIN = 500
|
||||
MIN_DRAW_DISTANCE = 10.0
|
||||
@@ -297,7 +298,10 @@ class ModelRenderer(Widget):
|
||||
def _get_ll_color(self, prob: float, adjacent: bool, left: bool):
|
||||
alpha = np.clip(prob, 0.0, 0.7)
|
||||
if adjacent:
|
||||
_base_color = LANE_LINE_COLORS.get(ui_state.status, LANE_LINE_COLORS[UIStatus.DISENGAGED])
|
||||
if gui_app.iqpilot_ui() and ui_state.status == UIStatus.ENGAGED:
|
||||
_base_color = NeonTheme.glow()
|
||||
else:
|
||||
_base_color = LANE_LINE_COLORS.get(ui_state.status, LANE_LINE_COLORS[UIStatus.DISENGAGED])
|
||||
color = rl.Color(_base_color.r, _base_color.g, _base_color.b, int(alpha * 255))
|
||||
|
||||
# turn adjacent lls orange if torque is high
|
||||
|
||||
@@ -424,6 +424,13 @@ class UIState(IQUIState):
|
||||
|
||||
self._started_prev = self.started
|
||||
|
||||
def measured_steer_delay(self) -> float | None:
|
||||
# estimatord is onroad-only, so offroad the lateralDelay socket reads back a default 0.0
|
||||
if self.sm.alive['lateralDelay']:
|
||||
return float(self.sm['lateralDelay'].lateralDelay)
|
||||
lag = log_param_from_bytes(self.params, "LiveDelay", log.Event)
|
||||
return float(lag.lateralDelay.lateralDelay) if lag is not None else None
|
||||
|
||||
def update_params(self) -> None:
|
||||
CP = log_param_from_bytes(self.params, "CarParamsPersistent", car.CarParams)
|
||||
if CP is not None:
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from iqpilot.cereal import messaging
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.common.realtime import Ratekeeper
|
||||
from iqpilot.common.swaglog import cloudlog
|
||||
|
||||
ANDROID_ROOT = "/data/android"
|
||||
HEADLESS = f"{ANDROID_ROOT}/waydroid_headless.sh"
|
||||
TRIM = f"{ANDROID_ROOT}/android_trim.sh"
|
||||
GUARD = f"{ANDROID_ROOT}/android_guard.sh"
|
||||
LXC_ATTACH = f"{ANDROID_ROOT}/root/usr/bin/lxc-attach"
|
||||
LD = f"{ANDROID_ROOT}/root/usr/lib/aarch64-linux-gnu"
|
||||
|
||||
# Waze's launcher is FreeMapAppActivity; there is no com.waze.MainActivity, and asking for
|
||||
# one fails with "Activity class does not exist" rather than anything that names the problem.
|
||||
NAV_APPS = {
|
||||
"waze": ("com.waze", "com.waze/.FreeMapAppActivity"),
|
||||
"maps": ("com.google.android.apps.maps", "com.google.android.apps.maps/com.google.android.maps.MapsActivity"),
|
||||
}
|
||||
BRIDGE_HOST = "192.168.240.112"
|
||||
BRIDGE_PORT = 8099
|
||||
GPS_SOURCES = ["iqLiveLocation", "liveLocationKalman", "gpsLocationExternal", "gpsLocation"]
|
||||
PROVIDERS = ("gps", "fused", "network")
|
||||
GPS_INTERVAL_S = 0.5
|
||||
|
||||
|
||||
def attach(*args: str, timeout: float = 10.0) -> subprocess.CompletedProcess:
|
||||
cmd = ["sudo", "-n", "env", f"LD_LIBRARY_PATH={LD}", LXC_ATTACH,
|
||||
"-P", "/var/lib/waydroid/lxc", "-n", "waydroid", "--", *args]
|
||||
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=False)
|
||||
|
||||
|
||||
def container_booted() -> bool:
|
||||
try:
|
||||
return attach("/system/bin/getprop", "sys.boot_completed", timeout=8).stdout.strip() == "1"
|
||||
except (subprocess.SubprocessError, OSError):
|
||||
return False
|
||||
|
||||
|
||||
def bring_up() -> None:
|
||||
# AGNOS mounts / read-only, so no unit file can be installed; a transient unit is the only
|
||||
# way the compositor and lxc-start survive this process exiting.
|
||||
subprocess.run(["sudo", "-n", "systemd-run", "--unit=iq-android", "--service-type=oneshot",
|
||||
"--remain-after-exit", HEADLESS, "up"],
|
||||
capture_output=True, text=True, timeout=120, check=False)
|
||||
|
||||
|
||||
def start_guard() -> None:
|
||||
subprocess.run(["sudo", "-n", "systemd-run", "--unit=iq-android-guard", GUARD],
|
||||
capture_output=True, text=True, timeout=30, check=False)
|
||||
|
||||
|
||||
def trim(nav_app: str) -> None:
|
||||
subprocess.run(["sudo", "-n", "env", f"ANDROID_NAV_APP={nav_app}", TRIM],
|
||||
capture_output=True, timeout=240, check=False)
|
||||
|
||||
|
||||
def enable_mock_location() -> None:
|
||||
# Wiped by every container restart, and the package form does not cover uid 0: without the
|
||||
# --uid form every injection fails with SecurityException and the apps just say "no GPS".
|
||||
attach("/system/bin/cmd", "appops", "set", "--uid", "0", "android:mock_location", "allow")
|
||||
for p in PROVIDERS:
|
||||
attach("/system/bin/cmd", "location", "providers", "add-test-provider", p)
|
||||
attach("/system/bin/cmd", "location", "providers", "set-test-provider-enabled", p, "true")
|
||||
# The a11y tree is only updated while the display is awake; asleep it emits nothing at all.
|
||||
attach("/system/bin/svc", "power", "stayon", "true")
|
||||
|
||||
|
||||
def inject(lat: float, lon: float) -> None:
|
||||
for p in PROVIDERS:
|
||||
attach("/system/bin/cmd", "location", "providers", "set-test-provider-location", p,
|
||||
"--location", f"{lat:.6f},{lon:.6f}", "--accuracy", "4", timeout=6)
|
||||
|
||||
|
||||
def read_position(sm: messaging.SubMaster) -> tuple[float, float] | None:
|
||||
for src in GPS_SOURCES:
|
||||
if src not in sm.data or not sm.valid.get(src, False):
|
||||
continue
|
||||
msg = sm[src]
|
||||
for lat_a, lon_a in (("latitude", "longitude"), ("lat", "lon")):
|
||||
lat = getattr(msg, lat_a, None)
|
||||
lon = getattr(msg, lon_a, None)
|
||||
if lat is not None and lon is not None and (lat or lon):
|
||||
return float(lat), float(lon)
|
||||
return None
|
||||
|
||||
|
||||
class AndroidNavDaemon:
|
||||
def __init__(self) -> None:
|
||||
self.params = Params()
|
||||
self.sources = [s for s in GPS_SOURCES if s in messaging.SERVICE_LIST]
|
||||
self.sm = messaging.SubMaster(self.sources) if self.sources else None
|
||||
self.last_inject = 0.0
|
||||
self.up = False
|
||||
|
||||
def status(self, text: str) -> None:
|
||||
self.params.put("IQAndroidNavStatus", text)
|
||||
|
||||
def sign_in(self) -> None:
|
||||
email = self.params.get("IQAndroidNavEmail", encoding="utf8")
|
||||
password = self.params.get("IQAndroidNavPassword", encoding="utf8")
|
||||
if not email or not password:
|
||||
return
|
||||
try:
|
||||
with socket.create_connection((BRIDGE_HOST, BRIDGE_PORT), timeout=10) as s:
|
||||
s.sendall(json.dumps({"cmd": "signin", "email": email, "password": password}).encode() + b"\n")
|
||||
self.status("sign-in requested")
|
||||
except OSError:
|
||||
cloudlog.exception("androidd: sign-in request failed")
|
||||
self.status("sign-in failed")
|
||||
finally:
|
||||
# Never let the credential outlive the one use it was handed over for.
|
||||
self.params.remove("IQAndroidNavEmail")
|
||||
self.params.remove("IQAndroidNavPassword")
|
||||
|
||||
def nav_app(self) -> str:
|
||||
value = self.params.get("IQAndroidNavApp", encoding="utf8") or "waze"
|
||||
return value if value in NAV_APPS else "waze"
|
||||
|
||||
def enforce_single_app(self, nav_app: str) -> None:
|
||||
for name, (pkg, _) in NAV_APPS.items():
|
||||
if name != nav_app:
|
||||
attach("/system/bin/am", "force-stop", pkg)
|
||||
|
||||
def ensure_app_running(self, nav_app: str) -> None:
|
||||
pkg, component = NAV_APPS[nav_app]
|
||||
if attach("/system/bin/pidof", pkg, timeout=8).stdout.strip():
|
||||
return
|
||||
attach("/system/bin/am", "start", "-n", component, timeout=20)
|
||||
|
||||
def ensure_up(self) -> None:
|
||||
if container_booted():
|
||||
if not self.up:
|
||||
nav_app = self.nav_app()
|
||||
enable_mock_location()
|
||||
trim(nav_app)
|
||||
self.enforce_single_app(nav_app)
|
||||
start_guard()
|
||||
self.status(f"running:{nav_app}")
|
||||
self.up = True
|
||||
return
|
||||
self.up = False
|
||||
self.status("starting")
|
||||
bring_up()
|
||||
|
||||
def step(self) -> None:
|
||||
if not self.params.get_bool("IQAndroidNav"):
|
||||
if self.up:
|
||||
subprocess.run(["sudo", "-n", "systemctl", "stop", "iq-android", "iq-android-guard"],
|
||||
capture_output=True, timeout=60, check=False)
|
||||
self.up = False
|
||||
self.status("disabled")
|
||||
return
|
||||
|
||||
self.ensure_up()
|
||||
if not self.up:
|
||||
return
|
||||
|
||||
self.sign_in()
|
||||
self.ensure_app_running(self.nav_app())
|
||||
|
||||
if self.sm is None:
|
||||
return
|
||||
self.sm.update(0)
|
||||
now = time.monotonic()
|
||||
if now - self.last_inject < GPS_INTERVAL_S:
|
||||
return
|
||||
position = read_position(self.sm)
|
||||
if position is not None:
|
||||
self.last_inject = now
|
||||
try:
|
||||
inject(*position)
|
||||
except subprocess.SubprocessError:
|
||||
cloudlog.exception("androidd: location inject failed")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not os.path.exists(HEADLESS):
|
||||
cloudlog.warning("androidd: android stack not staged, exiting")
|
||||
return
|
||||
daemon = AndroidNavDaemon()
|
||||
rk = Ratekeeper(2.0, print_delay_threshold=None)
|
||||
while True:
|
||||
try:
|
||||
daemon.step()
|
||||
except Exception:
|
||||
cloudlog.exception("androidd: step failed")
|
||||
rk.keep_time()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -427,8 +427,6 @@ def hardware_thread(end_event, hw_queue) -> None:
|
||||
|
||||
# Set ignition based on any panda connected
|
||||
onroad_conditions["ignition"] = any(ps.ignitionLine or ps.ignitionCan for ps in pandaStates if ps.pandaType != log.PandaState.PandaType.unknown)
|
||||
if params.get_bool("IQBenchIgnition"):
|
||||
onroad_conditions["ignition"] = True
|
||||
|
||||
pandaState = pandaStates[0]
|
||||
|
||||
@@ -439,6 +437,9 @@ def hardware_thread(end_event, hw_queue) -> None:
|
||||
onroad_conditions["ignition"] = False
|
||||
cloudlog.error("panda timed out onroad")
|
||||
|
||||
if params.get_bool("IQBenchIgnition"):
|
||||
onroad_conditions["ignition"] = True
|
||||
|
||||
# Run at 2Hz, plus either edge of ignition
|
||||
ign_edge = (started_ts is not None) != all(onroad_conditions.values())
|
||||
if (sm.frame % round(SERVICE_LIST['pandaStates'].frequency * DT_HW) != 0) and not ign_edge:
|
||||
@@ -470,7 +471,7 @@ def hardware_thread(end_event, hw_queue) -> None:
|
||||
online_cpu_usage = [int(round(n)) for n in psutil.cpu_percent(percpu=True)]
|
||||
offline_cpu_usage = [0., ] * (len(msg.deviceState.cpuTempC) - len(online_cpu_usage))
|
||||
msg.deviceState.cpuUsagePercent = online_cpu_usage + offline_cpu_usage
|
||||
if msg.deviceState.memoryUsagePercent > 85:
|
||||
if msg.deviceState.memoryUsagePercent > 95:
|
||||
avg_cpu_usage = int(round(sum(online_cpu_usage) / max(1, len(online_cpu_usage))))
|
||||
perf.emit(
|
||||
"hardware_low_memory",
|
||||
|
||||
@@ -67,7 +67,7 @@ def qcomgps(started: bool, params: Params, CP: car.CarParams) -> bool:
|
||||
def always_run(started: bool, params: Params, CP: car.CarParams) -> bool:
|
||||
return True
|
||||
|
||||
def android_nav(started: bool, params: Params, CP: car.CarParams) -> bool:
|
||||
def nav_assist(started: bool, params: Params, CP: car.CarParams) -> bool:
|
||||
return params.get_bool("IQAndroidNav")
|
||||
|
||||
def only_onroad(started: bool, params: Params, CP: car.CarParams) -> bool:
|
||||
@@ -179,7 +179,7 @@ procs = [
|
||||
PythonProcess("journald", "iqpilot.system.journald", only_onroad, platform.system() != "Darwin"),
|
||||
PythonProcess("micd", "iqpilot.system.micd", or_(iscar, livestream)),
|
||||
PythonProcess("timed", "iqpilot.system.timed", always_run, enabled=not PC),
|
||||
PythonProcess("androidd", "iqpilot.system.android.androidd", android_nav, enabled=TICI, restart_if_crash=True),
|
||||
BundleProcess("navassistd", "iqpilot_navassist_private", "iqpilot_private.navassist.daemon", nav_assist, enabled=TICI, restart_if_crash=True),
|
||||
|
||||
PythonProcess("dmonitoringmodeld", "iqpilot.selfdrive.dmonitoringmodeld.dmonitoringmodeld", driver_monitoring, enabled=not PC),
|
||||
|
||||
|
||||
@@ -900,7 +900,11 @@ class SteeringLayout(Widget):
|
||||
delay_desc = tr("Let IQ.Pilot measure how long your steering takes to respond and keep that figure up to date. "
|
||||
"Switch it off to pin the timing yourself.")
|
||||
if live_delay:
|
||||
delay_desc += f"<br>{tr('Measured:')} {ui_state.sm['lateralDelay'].lateralDelay:.3f} s"
|
||||
measured = ui_state.measured_steer_delay()
|
||||
if measured is None:
|
||||
delay_desc += f"<br>{tr('Measured:')} {tr('not yet, drive to calibrate')}"
|
||||
else:
|
||||
delay_desc += f"<br>{tr('Measured:')} {measured:.3f} s"
|
||||
elif ui_state.CP:
|
||||
sw = float(ui_state.params.get("IQSoftwareSteerDelay", "0.2"))
|
||||
cp = ui_state.CP.steerActuatorDelay
|
||||
|
||||
@@ -9,10 +9,10 @@ from iqpilot.system.ui.iqwidgets.lib import canvas
|
||||
from iqpilot.system.ui.lib.application import gui_app
|
||||
from iqpilot.ui.onroad.hud_overlays import IQSpeedLimitOverlay, _SL_ASSIST, _SL_DARK, _dim
|
||||
|
||||
_SIGN_X = 16
|
||||
_SIGN_Y = 108
|
||||
_SIGN_W = 60
|
||||
_SIGN_H = 64
|
||||
_SIGN_X = 17
|
||||
_SIGN_Y = 86
|
||||
_SIGN_W = 58
|
||||
_SIGN_H = 74
|
||||
_BADGE_SIDE = 24
|
||||
|
||||
|
||||
@@ -47,8 +47,8 @@ class MiciSpeedLimitSign(IQSpeedLimitOverlay):
|
||||
(self._vienna if ui_state.is_metric else self._mutcd)(box, value, badge, tint, has_limit, alpha)
|
||||
|
||||
def _vienna(self, rect, value, badge, tint, has_limit, alpha=1.0):
|
||||
hub = canvas.Pt(rect.x + rect.width / 2, rect.y + rect.height / 2)
|
||||
radius = rect.width / 2
|
||||
hub = canvas.Pt(rect.x + radius, rect.y + rect.height / 2)
|
||||
canvas.disc_at(hub, radius, _dim(canvas.WHITE, alpha))
|
||||
canvas.annulus(hub, radius * 0.78, radius, 0, 360, 36, _dim(canvas.RED, alpha))
|
||||
canvas.glyphs_centered(self._bold, value, 22 if len(value) >= 3 else 28, hub, _dim(tint, alpha))
|
||||
@@ -64,9 +64,9 @@ class MiciSpeedLimitSign(IQSpeedLimitOverlay):
|
||||
inner = canvas.Box(rect.x + 4, rect.y + 4, rect.width - 8, rect.height - 8)
|
||||
canvas.panel_outline(inner, 0.25, 8, 2, _dim(canvas.BLACK, alpha))
|
||||
mid = rect.x + rect.width / 2
|
||||
canvas.glyphs_centered(self._demi, "SPEED", 12, canvas.Pt(mid, rect.y + 14), _dim(canvas.BLACK, alpha))
|
||||
canvas.glyphs_centered(self._demi, "LIMIT", 12, canvas.Pt(mid, rect.y + 25), _dim(canvas.BLACK, alpha))
|
||||
canvas.glyphs_centered(self._bold, value, 30 if len(value) <= 2 else 24, canvas.Pt(mid, rect.y + 45), _dim(tint, alpha))
|
||||
canvas.glyphs_centered(self._demi, "SPEED", 13, canvas.Pt(mid, rect.y + 16), _dim(canvas.BLACK, alpha))
|
||||
canvas.glyphs_centered(self._demi, "LIMIT", 13, canvas.Pt(mid, rect.y + 29), _dim(canvas.BLACK, alpha))
|
||||
canvas.glyphs_centered(self._bold, value, 34 if len(value) <= 2 else 27, canvas.Pt(mid, rect.y + 52), _dim(tint, alpha))
|
||||
if badge:
|
||||
chip = canvas.Box(rect.x + rect.width - _BADGE_SIDE * 0.55, rect.y - _BADGE_SIDE * 0.55, _BADGE_SIDE, _BADGE_SIDE)
|
||||
canvas.panel(chip, 0.35, 8, _dim(canvas.BLACK, alpha))
|
||||
|
||||
Reference in New Issue
Block a user