Compare commits

...

38 Commits

Author SHA1 Message Date
royjr 13f5ae7a2d Update opendbc_repo 2026-03-05 22:10:29 -05:00
royjr 0f86cff7a3 Merge branch 'master' into henc 2026-03-05 20:53:10 -05:00
royjr 59270d641a Update opendbc_repo 2026-03-05 20:53:03 -05:00
Jason Wen 6dd72973ec [TIZI/TICI] ui: more add back gate steering arc behind toggle 2026-03-05 18:13:36 -05:00
Jason Wen 4e0a26be8d [TIZI/TICI] ui: add back gate steering arc behind toggle (#1756) 2026-03-05 17:03:22 -05:00
Lukas Heintz 7c3759e147 Rivian: Flash xnor's Longitudinal Upgrade Kit prior supported panda check (#1752)
* fixed missing internal panda

* lets do it like that

* cleanup

* move up

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-03-05 12:34:08 -05:00
Jason Wen baaa2704ee Sync: commaai/openpilot:mastersunnypilot/sunnypilot:master (#1755) 2026-03-05 01:43:50 -05:00
Jason Wen 00afa068a1 Merge branch 'upstream/openpilot/master' into sync-20260304
# Conflicts:
#	selfdrive/ui/mici/layouts/onboarding.py
2026-03-05 01:27:07 -05:00
Jason Wen 6bea70ac86 pandad: gate unsupported pandas before flashing (#1754) 2026-03-04 23:15:10 -05:00
Adeeb Shihadeh fc372e2ae1 ui needs pillow 2026-03-04 12:36:40 -08:00
Adeeb Shihadeh cd22ee3327 rm openssl3 package (#37551)
* rm openssl3 package

* upgrade

* lil more
2026-03-04 09:50:23 -08:00
Shane Smiskol e97a1d1a44 updater: zipapp and additional fixes (#37550)
* new updater zipapp

* fix deadlock from agnos.py throwing timeout errors, never hitting failed screen! + try catch the whole process for errors while starting process

* add todo

* set core affinity like setup in updater

* fix import

* rezip
2026-03-04 04:34:48 -08:00
Shane Smiskol 6795b09d0a file_downloader: stream downloads in a single HTTP request (#37549)
The Python file downloader was making a separate HTTP Range request per
1MB chunk via URLFile.read(), causing massive latency overhead. Use a
single streaming GET request instead, matching the old C++ behavior.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 03:16:29 -08:00
Shane Smiskol 20d484c7cb reset: recover needs to reboot (#37546)
fix not rebooting
2026-03-04 01:23:56 -08:00
Shane Smiskol 7e1a8d41a1 steering arc: enable for angle cars (#37078)
* enable for angle cars

* use carparams

* less roll at low speed, it's too pronounced

* clean up
2026-03-03 21:45:49 -08:00
royjr 0c452dbafe cabana: fix right pane width limitation (#37527)
Update chartswidget.cc
2026-03-03 20:12:53 -08:00
Shane Smiskol 56ed377197 Zipapp fixes (#37538)
* zip app fixes

* add nl

* rename

* emoji was brok

* bytes
2026-03-03 17:23:48 -08:00
Shane Smiskol 92f9684fdb Revert "use vendored raylib from dependencies repo" (#37537)
Revert "use vendored raylib from dependencies repo (#37489)"

This reverts commit 0374979397.
2026-03-03 01:13:11 -08:00
Shane Smiskol 91b7752268 Setup: improvements (#37264)
* pressed state for larger sliders

* wifibutton

* fix

* clean up

* some work

* don't nee this now

* stash

* more

* new pressed bigcircle

* black

* interp

* just check position

* clean up and fix slider reset

* fix custom

* no speed

* stash

* even chatter couldn't figure this one out

* makes sense to combine together, less split mentality

* clean that up

* fix lag

* match ui.py prio to eliminate lag on wifiui show event. separately, why is this slow?

* night mode

* delay scroll over

* fix auto scrolling

* stash

* waiting looks disabled

* clean up and don't reset sliders until user goes back

* rm

* fix

* add termsheader back

* fix callbacks

* ctrl alt l

* fix text spacing

* clean up

* stash

* fix style

* i want to go back

* guard on exit

* kinda useless stuff

* Revert "kinda useless stuff"

This reverts commit a4acbac31523408f358c5f68262cb630aa13ad8e.

* Revert "guard on exit"

This reverts commit 63ccfbf64edfbe1a144a441681f5ec78d8021ff7.

* wide

* setup pressed!

* grow animation

* 10s after initial

* slow fast

* start onboarding (terms)

* rm duplicate page

* add qr code

* final grey

* fix visual lag on first start

* clean up dead code

* dont exit from cancel

* revert grey

* clean up, REVIEW ME

* Revert "clean up, REVIEW ME"

This reverts commit c66fa60947c5f922520e7cf58c630b4bbe2d0177.

* reboot slider

* kb fix

* Revert "kb fix"

This reverts commit 883039448e6c37ae1d25d4f75ada6e96b6736358.

* ./ goes to letters

* Revert "./ goes to letters"

This reverts commit 0d97442427edb1a000638863a3f2181204ddc160.

* clean up

* some more clean up

* more

* clean up

* rename block

* reset pending scroll so it can't use stale data in rare sequence

* remove unused assets

* clean up imports

* fix updater

* clean up

* fix double reboot

* demo time - reset to setup on reboot

* let manager restart

* Revert "demo time - reset to setup on reboot"

This reverts commit 9468657e8438a1ce8fcb5266403b7bb3539f131f.

* url... and no grow animation on start button

* one next button

* grow instead of shake wifi button

* 36 pt font size in setup

* touch up onboarding a lil

* Revert "rm cpp bz2 (#37332)"

This reverts commit f4a36f7f74.

* more onboarding and clean up

* clean up

* wow what an amazing future clean up

* back to software select

* fix

* copy

* fix dm confirmation dialog not disabling widget underneath, all fixed with real nav stack in here

* uploading

* lint

* add review terms to device w/ close button

* todo

* remove old Terms vertical scrolling classes

* use new Scroller!

* installer

* tweak to match figma exactly

* revert

* fixup updater

* demo day

* demo day v2

* ... for percent while finishing setup

* demo day v3

* demo day v4

* remove ...

* demo day v6 -- "why does it do that!!"

* demo day v7 -- no flash

* hmm

* demo day v7

* prebuilt

* revert demo day

* scroll after pop animation

* back -> retry

* stash fixes

* damn, need back_callback

* scroll over immediately if already in network setup

* tweaks

* going down is confusing

* more

* Revert "more"

This reverts commit 29ce75b1f81eb40e7527a71d27842d9a66802206.

* Revert "going down is confusing"

This reverts commit 0cd2ae30d4135db1ccba6478429b45e886714e9c.

* dupl

* nl

* sort functions

* more clean up from merge

* move

* more

* dismiss to download (hack)

* Revert "dismiss to download (hack)"

This reverts commit 53c45ed1f63db1f0cebbce0dfab1777c8658f505.

* onboarding work

* set brightness and timeout in root onboarding only

* clean up

* type

* keep 5m for settings preview

* switch back to letters on . or /

* reset first step scroller

* custom software warning goes down network comes up and back cb fix

* clean up

* smaller qr

* ReviewTermsPage just for device as NavWidget

* clean up

* installer: stay on 100%

* reset has internet while in wifiui

* try this

* try this

* see what error we get exactly

see what error we get exactly

* not final solution but see how good

* rm

* copy changes

* reset on disconnect

* for separate pr

* Revert "reset on disconnect"

This reverts commit 552372fa4d497ba7d9de7f2edb730ee63798ffa4.

* revert this, too buggy

* fix for updater

* sort

* fix test

* minor cleanup

* more leaks than this rn

* onboarding clean up

* clean up application

* click delay to small button

* clean up

* reset more state

* fix training guide not cleaning up driverview

* Revert "fix training guide not cleaning up driverview"

This reverts commit cac7c5f436056cc9e747f80905d390790fb83c22.

* simpler fix :(

* nice catch, if you go back to terms it will reset 300s timeout and brightness

* duplicate show

* unused
2026-03-03 01:06:51 -08:00
Shane Smiskol 2ebf09eb07 Clear frame on offroad transition 2026-03-02 23:25:23 -08:00
Shane Smiskol 90af6be9b8 Render offroad text centered 2026-03-02 23:24:43 -08:00
Shane Smiskol 3504ccb639 ui: keyboard goes back on . or / (#37534)
switch back to letters on . or /
2026-03-02 19:49:50 -08:00
Shane Smiskol 443cd795a3 Onboarding: set real width 2026-03-02 15:37:18 -08:00
royjr 06b2c68e03 macOS: fix cabana builds (#37518) 2026-03-01 18:14:41 -08:00
Adeeb Shihadeh 3478ac1338 cabana: remove QtSerialBus (#37523) 2026-03-01 16:12:04 -08:00
Adeeb Shihadeh ce04d25f7d cabana: remove QtConcurrent (#37522) 2026-03-01 16:00:29 -08:00
Adeeb Shihadeh 0c7abf3855 cabana: remove QtXml (#37521) 2026-03-01 15:55:57 -08:00
Adeeb Shihadeh 0b9ab8bb91 cabana: replace Qt types with stdlib (#37519)
* cabana: replace Qt types with stdlib

* lil more

* cleanup sconscript
2026-03-01 15:51:16 -08:00
Adeeb Shihadeh 6b52ee7ef2 tools cleanup (#37520) 2026-03-01 15:40:10 -08:00
Adeeb Shihadeh c3d5c5f016 fix nigthly build (#37516) 2026-03-01 14:12:27 -08:00
Adeeb Shihadeh 0374979397 use vendored raylib from dependencies repo (#37489) 2026-03-01 13:52:39 -08:00
royjr 55f6a8b4f5 Update opendbc_repo 2026-02-28 02:22:34 -05:00
royjr dd1e84ccce Update opendbc_repo 2026-02-28 01:54:13 -05:00
royjr c6575b5870 Update opendbc_repo 2026-02-28 01:43:06 -05:00
royjr 0498bdb532 Update opendbc_repo 2026-02-28 00:35:35 -05:00
royjr ffb2d66e8c Update opendbc_repo 2026-02-28 00:18:36 -05:00
royjr f726c2392a Update opendbc_repo 2026-02-28 00:10:51 -05:00
royjr 3c32b61d7f Update opendbc_repo 2026-02-27 14:15:20 -05:00
97 changed files with 1435 additions and 3471 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
running-workflow-name: 'build __nightly' running-workflow-name: 'build __nightly'
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
check-regexp: ^((?!.*(build prebuilt|create badges).*).)*$ check-regexp: ^((?!.*(build prebuilt|create badges).*).)*$
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with: with:
submodules: true submodules: true
fetch-depth: 0 fetch-depth: 0
+1 -2
View File
@@ -46,11 +46,10 @@ if arch != "larch64":
import libjpeg import libjpeg
import libyuv import libyuv
import ncurses import ncurses
import openssl3
import python3_dev import python3_dev
import zeromq import zeromq
import zstd import zstd
pkgs = [bzip2, capnproto, eigen, ffmpeg_pkg, libjpeg, libyuv, ncurses, openssl3, zeromq, zstd] pkgs = [bzip2, capnproto, eigen, ffmpeg_pkg, libjpeg, libyuv, ncurses, zeromq, zstd]
py_include = python3_dev.INCLUDE_DIR py_include = python3_dev.INCLUDE_DIR
else: else:
# TODO: remove when AGNOS has our new vendor pkgs # TODO: remove when AGNOS has our new vendor pkgs
+2 -3
View File
@@ -32,12 +32,12 @@ dependencies = [
"ffmpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ffmpeg", "ffmpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ffmpeg",
"libjpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=libjpeg", "libjpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=libjpeg",
"libyuv @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=libyuv", "libyuv @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=libyuv",
"openssl3 @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=openssl3",
"python3-dev @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=python3-dev", "python3-dev @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=python3-dev",
"zstd @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zstd", "zstd @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zstd",
"ncurses @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ncurses", "ncurses @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ncurses",
"zeromq @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zeromq", "zeromq @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zeromq",
"git-lfs @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=git-lfs", "git-lfs @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=git-lfs",
"gcc-arm-none-eabi @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=gcc-arm-none-eabi",
# body / webrtcd # body / webrtcd
"av", "av",
@@ -76,6 +76,7 @@ dependencies = [
"raylib > 5.5.0.3", "raylib > 5.5.0.3",
"qrcode", "qrcode",
"jeepney", "jeepney",
"pillow",
] ]
[project.optional-dependencies] [project.optional-dependencies]
@@ -103,12 +104,10 @@ testing = [
dev = [ dev = [
"matplotlib", "matplotlib",
"opencv-python-headless", "opencv-python-headless",
"gcc-arm-none-eabi @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=gcc-arm-none-eabi",
] ]
tools = [ tools = [
"metadrive-simulator @ git+https://github.com/commaai/metadrive.git@minimal ; (platform_machine != 'aarch64')", "metadrive-simulator @ git+https://github.com/commaai/metadrive.git@minimal ; (platform_machine != 'aarch64')",
"dearpygui>=2.1.0; (sys_platform != 'linux' or platform_machine != 'aarch64')", # not vended for linux aarch64
] ]
[project.urls] [project.urls]
+1 -1
View File
@@ -12,7 +12,7 @@ from openpilot.common.basedir import BASEDIR
DIRS = ['cereal', 'openpilot'] DIRS = ['cereal', 'openpilot']
EXTS = ['.png', '.py', '.ttf', '.capnp', '.json', '.fnt', '.mo'] EXTS = ['.png', '.py', '.ttf', '.capnp', '.json', '.fnt', '.mo', '.po']
INTERPRETER = '/usr/bin/env python3' INTERPRETER = '/usr/bin/env python3'
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a6892bd4d9b14b587fa491a6d608562e38819b4c618b1d7a3e8c384f05d52a2b
size 1245
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3428d8fcf2ecf9542c524706124f82b7fc809453c63418c9234ac9df5d85bd24
size 10074
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b2810add4943dd4f20a984ed6011b520925919a58d5c0dd0d846fc4d7f8a1d02
size 7109
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a3a87454a3d2f1ebb327211062c52480de945673dcfd137c5da3df8fa98d731
size 22400
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1f5ee67cd334d259ac33f932281db36533877009b5769c92d9cff3054fd5627c oid sha256:63c1499106621a4d927c21b2b04c87235a927216d9f513a0205f0fe03b8c799b
size 2942 size 12320
+21 -15
View File
@@ -32,8 +32,7 @@ def flash_panda(panda_serial: str) -> Panda:
raise raise
# skip flashing if the detected panda is not supported # skip flashing if the detected panda is not supported
supported_panda = check_panda_support(panda) if panda.get_type() not in Panda.SUPPORTED_DEVICES:
if not supported_panda:
cloudlog.warning(f"Panda {panda_serial} is not supported (hw_type: {panda.get_type()}), skipping flash...") cloudlog.warning(f"Panda {panda_serial} is not supported (hw_type: {panda.get_type()}), skipping flash...")
return panda return panda
@@ -69,10 +68,19 @@ def flash_panda(panda_serial: str) -> Panda:
return panda return panda
def check_panda_support(panda) -> bool: def check_panda_support(panda_serials: list[str]) -> bool:
hw_type = panda.get_type() unsupported = []
if hw_type in Panda.SUPPORTED_DEVICES: for serial in panda_serials:
return True panda = Panda(serial)
hw_type = panda.get_type()
panda.close()
if hw_type in Panda.SUPPORTED_DEVICES:
return True
unsupported.append((serial, hw_type))
for serial, hw_type in unsupported:
cloudlog.warning(f"Panda {serial} is not supported (hw_type: {hw_type}), skipping...")
return False return False
@@ -126,13 +134,17 @@ def main() -> None:
cloudlog.info(f"{len(panda_serials)} panda(s) found, connecting - {panda_serials}") cloudlog.info(f"{len(panda_serials)} panda(s) found, connecting - {panda_serials}")
# custom flasher for xnor's Rivian Longitudinal Upgrade Kit
flash_rivian_long(panda_serials)
# skip flashing and health check if no supported panda is detected
if not check_panda_support(panda_serials):
continue
# Flash the first panda # Flash the first panda
panda_serial = panda_serials[0] panda_serial = panda_serials[0]
panda = flash_panda(panda_serial) panda = flash_panda(panda_serial)
# flash Rivian longitudinal upgrade panda
flash_rivian_long(panda)
# Ensure internal panda is present if expected # Ensure internal panda is present if expected
if HARDWARE.has_internal_panda() and not panda.is_internal(): if HARDWARE.has_internal_panda() and not panda.is_internal():
cloudlog.error("Internal panda is missing, trying again") cloudlog.error("Internal panda is missing, trying again")
@@ -143,12 +155,6 @@ def main() -> None:
# log panda fw version # log panda fw version
params.put("PandaSignatures", panda.get_signature()) params.put("PandaSignatures", panda.get_signature())
# skip health check if the detected panda is not supported
supported_panda = check_panda_support(panda)
if not supported_panda:
cloudlog.warning(f"Panda {panda.get_usb_serial()} is not supported (hw_type: {panda.get_type()}), skipping health check...")
continue
# check health for lost heartbeat # check health for lost heartbeat
health = panda.health() health = panda.health()
if health["heartbeat_lost"]: if health["heartbeat_lost"]:
+5 -7
View File
@@ -81,16 +81,14 @@ void run(const char* cmd) {
} }
void finishInstall() { void finishInstall() {
BeginDrawing(); if (tici_device) {
ClearBackground(BLACK); BeginDrawing();
if (tici_device) { ClearBackground(BLACK);
const char *m = "Finishing install..."; const char *m = "Finishing install...";
int text_width = MeasureText(m, FONT_SIZE); int text_width = MeasureText(m, FONT_SIZE);
DrawTextEx(font_display, m, (Vector2){(float)(GetScreenWidth() - text_width)/2 + FONT_SIZE, (float)(GetScreenHeight() - FONT_SIZE)/2}, FONT_SIZE, 0, WHITE); DrawTextEx(font_display, m, (Vector2){(float)(GetScreenWidth() - text_width)/2 + FONT_SIZE, (float)(GetScreenHeight() - FONT_SIZE)/2}, FONT_SIZE, 0, WHITE);
} else { EndDrawing();
DrawTextEx(font_display, "finishing setup", (Vector2){12, 0}, 77, 0, (Color){255, 255, 255, (unsigned char)(255 * 0.9)}); }
}
EndDrawing();
util::sleep_for(60 * 1000); util::sleep_for(60 * 1000);
} }
+3 -3
View File
@@ -56,7 +56,7 @@ class MiciMainLayout(Scroller):
gui_app.push_widget(self) gui_app.push_widget(self)
# Start onboarding if terms or training not completed, make sure to push after self # Start onboarding if terms or training not completed, make sure to push after self
self._onboarding_window = OnboardingWindow() self._onboarding_window = OnboardingWindow(lambda: gui_app.pop_widgets_to(self))
if not self._onboarding_window.completed: if not self._onboarding_window.completed:
gui_app.push_widget(self._onboarding_window) gui_app.push_widget(self._onboarding_window)
@@ -82,7 +82,7 @@ class MiciMainLayout(Scroller):
def _handle_transitions(self): def _handle_transitions(self):
# Don't pop if onboarding # Don't pop if onboarding
if gui_app.get_active_widget() == self._onboarding_window: if gui_app.widget_in_stack(self._onboarding_window):
return return
if ui_state.started != self._prev_onroad: if ui_state.started != self._prev_onroad:
@@ -108,7 +108,7 @@ class MiciMainLayout(Scroller):
def _on_interactive_timeout(self): def _on_interactive_timeout(self):
# Don't pop if onboarding # Don't pop if onboarding
if gui_app.get_active_widget() == self._onboarding_window: if gui_app.widget_in_stack(self._onboarding_window):
return return
if ui_state.started: if ui_state.started:
+237 -307
View File
@@ -1,32 +1,25 @@
from enum import IntEnum
import weakref
import math import math
import numpy as np import numpy as np
import qrcode
import pyray as rl import pyray as rl
from collections.abc import Callable
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import SmallButton, SmallCircleIconButton from openpilot.system.ui.widgets.button import SmallCircleIconButton
from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.scroller import NavScroller, Scroller
from openpilot.system.ui.widgets.slider import SmallSlider from openpilot.system.ui.widgets.nav_widget import NavWidget
from openpilot.system.ui.mici_setup import TermsHeader, TermsPage as SetupTermsPage from openpilot.system.ui.mici_setup import GreyBigButton, BigPillButton
from openpilot.selfdrive.ui.ui_state import ui_state, device
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import BaseDriverCameraDialog
from openpilot.system.ui.widgets.label import gui_label from openpilot.system.ui.widgets.label import gui_label
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.version import terms_version, training_version, terms_version_sp from openpilot.system.version import terms_version, training_version, terms_version_sp
from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkOnboarding from openpilot.selfdrive.ui.ui_state import ui_state, device
from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialogV2
class OnboardingState(IntEnum): from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
TERMS = 0 from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import BaseDriverCameraDialog
ONBOARDING = 1 from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkConsentPage
DECLINE = 2
SUNNYLINK_CONSENT = 3
class DriverCameraSetupDialog(BaseDriverCameraDialog): class DriverCameraSetupDialog(BaseDriverCameraDialog):
@@ -60,91 +53,62 @@ class DriverCameraSetupDialog(BaseDriverCameraDialog):
rl.end_scissor_mode() rl.end_scissor_mode()
class TrainingGuidePreDMTutorial(SetupTermsPage): class TrainingGuidePreDMTutorial(NavScroller):
def __init__(self, continue_callback): def __init__(self, continue_callback: Callable[[], None]):
super().__init__(continue_callback, continue_text="continue") super().__init__()
self._title_header = TermsHeader("driver monitoring setup", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
self._dm_label = UnifiedLabel("Next, we'll ensure comma four is mounted properly.\n\nIf it does not have a clear view of the driver, " + continue_button = BigPillButton("next")
"unplug and remount before continuing.", 42, continue_button.set_click_callback(continue_callback)
FontWeight.ROMAN)
self._scroller.add_widgets([
GreyBigButton("driver monitoring\ncheck", "scroll to continue",
gui_app.texture("icons_mici/setup/green_dm.png", 64, 64)),
GreyBigButton("", "Next, we'll check if comma four can detect the driver properly."),
GreyBigButton("", "sunnypilot uses the cabin camera to check if the driver is distracted."),
GreyBigButton("", "If it does not have a clear view of the driver, unplug and remount before continuing."),
continue_button,
])
def show_event(self): def show_event(self):
super().show_event() super().show_event()
# Get driver monitoring model ready for next step # Get driver monitoring model ready for next step
ui_state.params.put_bool("IsDriverViewEnabled", True) ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", True)
@property
def _content_height(self):
return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset()
def _render_content(self, scroll_offset):
self._title_header.render(rl.Rectangle(
self._rect.x + 16,
self._rect.y + 16 + scroll_offset,
self._title_header.rect.width,
self._title_header.rect.height,
))
self._dm_label.render(rl.Rectangle(
self._rect.x + 16,
self._title_header.rect.y + self._title_header.rect.height + 16,
self._rect.width - 32,
self._dm_label.get_content_height(int(self._rect.width - 32)),
))
class DMBadFaceDetected(SetupTermsPage): class DMBadFaceDetected(NavScroller):
def __init__(self, continue_callback, back_callback): def __init__(self):
super().__init__(continue_callback, back_callback, continue_text="power off") super().__init__()
self._title_header = TermsHeader("make sure comma four can see your face", gui_app.texture("icons_mici/setup/orange_dm.png", 60, 60))
self._dm_label = UnifiedLabel("Re-mount if your face is occluded or driver monitoring has difficulty tracking your face.", 42, FontWeight.ROMAN)
@property back_button = BigPillButton("back")
def _content_height(self): back_button.set_click_callback(self.dismiss)
return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset()
def _render_content(self, scroll_offset): self._scroller.add_widgets([
self._title_header.render(rl.Rectangle( GreyBigButton("looking for driver", "make sure comma\nfour can see your face",
self._rect.x + 16, gui_app.texture("icons_mici/setup/orange_dm.png", 64, 64)),
self._rect.y + 16 + scroll_offset, GreyBigButton("", "Remount if your face is blocked, or driver monitoring has difficulty tracking your face."),
self._title_header.rect.width, back_button,
self._title_header.rect.height, ])
))
self._dm_label.render(rl.Rectangle(
self._rect.x + 16,
self._title_header.rect.y + self._title_header.rect.height + 16,
self._rect.width - 32,
self._dm_label.get_content_height(int(self._rect.width - 32)),
))
class TrainingGuideDMTutorial(Widget): class TrainingGuideDMTutorial(NavWidget):
PROGRESS_DURATION = 4 PROGRESS_DURATION = 4
LOOKING_THRESHOLD_DEG = 30.0 LOOKING_THRESHOLD_DEG = 30.0
def __init__(self, continue_callback): def __init__(self, continue_callback: Callable[[], None]):
super().__init__() super().__init__()
self_ref = weakref.ref(self)
self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 28, 48)) self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 28, 48))
self._back_button.set_click_callback(lambda: self_ref() and self_ref()._show_bad_face_page()) self._back_button.set_click_callback(lambda: gui_app.push_widget(self._bad_face_page))
self._back_button.set_touch_valid_callback(lambda: self.enabled and not self.is_dismissing) # for nav stack
self._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 42, 42)) self._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 42, 42))
self._good_button.set_touch_valid_callback(lambda: self.enabled and not self.is_dismissing) # for nav stack
# Wrap the continue callback to restore settings self._good_button.set_click_callback(continue_callback)
def wrapped_continue_callback():
device.set_offroad_brightness(None)
continue_callback()
self._good_button.set_click_callback(wrapped_continue_callback)
self._good_button.set_enabled(False) self._good_button.set_enabled(False)
self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps) self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps)
self._dialog = DriverCameraSetupDialog() self._dialog = DriverCameraSetupDialog()
self._bad_face_page = DMBadFaceDetected(HARDWARE.shutdown, lambda: self_ref() and self_ref()._hide_bad_face_page()) self._bad_face_page = DMBadFaceDetected()
self._should_show_bad_face_page = False
# Disable driver monitoring model when device times out for inactivity # Disable driver monitoring model when device times out for inactivity
def inactivity_callback(): def inactivity_callback():
@@ -152,23 +116,11 @@ class TrainingGuideDMTutorial(Widget):
device.add_interactive_timeout_callback(inactivity_callback) device.add_interactive_timeout_callback(inactivity_callback)
def _show_bad_face_page(self):
self._bad_face_page.show_event()
self.hide_event()
self._should_show_bad_face_page = True
def _hide_bad_face_page(self):
self._bad_face_page.hide_event()
self.show_event()
self._should_show_bad_face_page = False
def show_event(self): def show_event(self):
super().show_event() super().show_event()
self._dialog.show_event() self._dialog.show_event()
self._progress.x = 0.0 self._progress.x = 0.0
device.set_offroad_brightness(100)
def _update_state(self): def _update_state(self):
super()._update_state() super()._update_state()
if device.awake and not ui_state.params.get_bool("IsDriverViewEnabled"): if device.awake and not ui_state.params.get_bool("IsDriverViewEnabled"):
@@ -188,7 +140,8 @@ class TrainingGuideDMTutorial(Widget):
looking_center = False looking_center = False
# stay at 100% once reached # stay at 100% once reached
if (dm_state.faceDetected and looking_center) or self._progress.x > 0.99: in_bad_face = gui_app.get_active_widget() == self._bad_face_page
if ((dm_state.faceDetected and looking_center) or self._progress.x > 0.99) and not in_bad_face:
slow = self._progress.x < 0.25 slow = self._progress.x < 0.25
duration = self.PROGRESS_DURATION * 2 if slow else self.PROGRESS_DURATION duration = self.PROGRESS_DURATION * 2 if slow else self.PROGRESS_DURATION
self._progress.x += 1.0 / (duration * gui_app.target_fps) self._progress.x += 1.0 / (duration * gui_app.target_fps)
@@ -199,9 +152,6 @@ class TrainingGuideDMTutorial(Widget):
self._good_button.set_enabled(self._progress.x >= 0.999) self._good_button.set_enabled(self._progress.x >= 0.999)
def _render(self, _): def _render(self, _):
if self._should_show_bad_face_page:
return self._bad_face_page.render(self._rect)
self._dialog.render(self._rect) self._dialog.render(self._rect)
rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - 80), rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - 80),
@@ -258,266 +208,246 @@ class TrainingGuideDMTutorial(Widget):
)) ))
# rounded border # rounded border
rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height))
rl.draw_rectangle_rounded_lines_ex(self._rect, 0.2 * 1.02, 10, 50, rl.BLACK) rl.draw_rectangle_rounded_lines_ex(self._rect, 0.2 * 1.02, 10, 50, rl.BLACK)
rl.end_scissor_mode()
class TrainingGuideRecordFront(SetupTermsPage): class TrainingGuideRecordFront(NavScroller):
def __init__(self, continue_callback): def __init__(self, continue_callback: Callable[[], None]):
def on_back():
ui_state.params.put_bool("RecordFront", False)
continue_callback()
def on_continue():
ui_state.params.put_bool("RecordFront", True)
continue_callback()
super().__init__(on_continue, back_callback=on_back, back_text="no", continue_text="yes")
self._title_header = TermsHeader("improve driver monitoring", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
self._dm_label = UnifiedLabel("Do you want to upload driver camera data?", 42,
FontWeight.ROMAN)
def show_event(self):
super().show_event()
# Disable driver monitoring model after last step
ui_state.params.put_bool("IsDriverViewEnabled", False)
@property
def _content_height(self):
return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset()
def _render_content(self, scroll_offset):
self._title_header.render(rl.Rectangle(
self._rect.x + 16,
self._rect.y + 16 + scroll_offset,
self._title_header.rect.width,
self._title_header.rect.height,
))
self._dm_label.render(rl.Rectangle(
self._rect.x + 16,
self._title_header.rect.y + self._title_header.rect.height + 16,
self._rect.width - 32,
self._dm_label.get_content_height(int(self._rect.width - 32)),
))
class TrainingGuideAttentionNotice(SetupTermsPage):
def __init__(self, continue_callback):
super().__init__(continue_callback, continue_text="continue")
self._title_header = TermsHeader("driver assistance", gui_app.texture("icons_mici/setup/warning.png", 60, 60))
self._warning_label = UnifiedLabel("1. sunnypilot is a driver assistance system.\n\n" +
"2. You must pay attention at all times.\n\n" +
"3. You must be ready to take over at any time.\n\n" +
"4. You are fully responsible for driving the car.", 42,
FontWeight.ROMAN)
@property
def _content_height(self):
return self._warning_label.rect.y + self._warning_label.rect.height - self._scroll_panel.get_offset()
def _render_content(self, scroll_offset):
self._title_header.render(rl.Rectangle(
self._rect.x + 16,
self._rect.y + 16 + scroll_offset,
self._title_header.rect.width,
self._title_header.rect.height,
))
self._warning_label.render(rl.Rectangle(
self._rect.x + 16,
self._title_header.rect.y + self._title_header.rect.height + 16,
self._rect.width - 32,
self._warning_label.get_content_height(int(self._rect.width - 32)),
))
class TrainingGuide(Widget):
def __init__(self, completed_callback=None):
super().__init__() super().__init__()
self._completed_callback = completed_callback
self._step = 0
self_ref = weakref.ref(self) def show_accept_dialog():
def on_accept():
ui_state.params.put_bool_nonblocking("RecordFront", True)
continue_callback()
def on_continue(): gui_app.push_widget(BigConfirmationDialogV2("allow data uploading", "icons_mici/setup/driver_monitoring/dm_check.png", exit_on_confirm=False,
if obj := self_ref(): confirm_callback=on_accept))
obj._advance_step()
def show_decline_dialog():
def on_decline():
ui_state.params.put_bool_nonblocking("RecordFront", False)
continue_callback()
gui_app.push_widget(BigConfirmationDialogV2("no, don't upload", "icons_mici/setup/cancel.png", exit_on_confirm=False, confirm_callback=on_decline))
self._accept_button = BigCircleButton("icons_mici/setup/driver_monitoring/dm_check.png")
self._accept_button.set_click_callback(show_accept_dialog)
self._decline_button = BigCircleButton("icons_mici/setup/cancel.png")
self._decline_button.set_click_callback(show_decline_dialog)
self._scroller.add_widgets([
GreyBigButton("driver camera data", "do you want to share video data for training?",
gui_app.texture("icons_mici/setup/green_dm.png", 64, 64)),
GreyBigButton("", "Sharing your data with comma helps improve openpilot and sunnypilot for everyone."),
self._accept_button,
self._decline_button,
])
class TrainingGuideAttentionNotice(Scroller):
def __init__(self, continue_callback: Callable[[], None]):
super().__init__()
continue_button = BigPillButton("next")
continue_button.set_click_callback(continue_callback)
self._scroller.add_widgets([
GreyBigButton("what is sunnypilot?", "scroll to continue",
gui_app.texture("icons_mici/setup/green_info.png", 64, 64)),
GreyBigButton("", "1. sunnypilot is a driver assistance system."),
GreyBigButton("", "2. You must pay attention at all times."),
GreyBigButton("", "3. You must be ready to take over at any time."),
GreyBigButton("", "4. You are fully responsible for driving the car."),
continue_button,
])
class TrainingGuide(NavWidget):
def __init__(self, completed_callback: Callable[[], None]):
super().__init__()
self._steps = [ self._steps = [
TrainingGuideAttentionNotice(continue_callback=on_continue), TrainingGuideAttentionNotice(continue_callback=lambda: gui_app.push_widget(self._steps[1])),
TrainingGuidePreDMTutorial(continue_callback=on_continue), TrainingGuidePreDMTutorial(continue_callback=lambda: gui_app.push_widget(self._steps[2])),
TrainingGuideDMTutorial(continue_callback=on_continue), TrainingGuideDMTutorial(continue_callback=lambda: gui_app.push_widget(self._steps[3])),
TrainingGuideRecordFront(continue_callback=on_continue), TrainingGuideRecordFront(continue_callback=completed_callback),
] ]
self._steps[0].set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack
def show_event(self): def show_event(self):
super().show_event() super().show_event()
device.set_override_interactive_timeout(300) self._steps[0].show_event()
def hide_event(self): def _render(self, _):
super().hide_event() self._steps[0].render(self._rect)
device.set_override_interactive_timeout(None)
def _advance_step(self):
if self._step < len(self._steps) - 1: class QRCodeWidget(Widget):
self._step += 1 def __init__(self, url: str, size: int = 170):
self._steps[self._step].show_event() super().__init__()
else: self.set_rect(rl.Rectangle(0, 0, size, size))
self._step = 0 self._size = size
if self._completed_callback: self._qr_texture: rl.Texture | None = None
self._completed_callback() self._generate_qr(url)
def _generate_qr(self, url: str):
qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=0)
qr.add_data(url)
qr.make(fit=True)
pil_img = qr.make_image(fill_color="white", back_color="black").convert('RGBA')
img_array = np.array(pil_img, dtype=np.uint8)
rl_image = rl.Image()
rl_image.data = rl.ffi.cast("void *", img_array.ctypes.data)
rl_image.width = pil_img.width
rl_image.height = pil_img.height
rl_image.mipmaps = 1
rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
self._qr_texture = rl.load_texture_from_image(rl_image)
def _render(self, _):
if self._qr_texture:
scale = self._size / self._qr_texture.height
rl.draw_texture_ex(self._qr_texture, rl.Vector2(self._rect.x, self._rect.y), 0.0, scale, rl.WHITE)
def __del__(self):
if self._qr_texture and self._qr_texture.id != 0:
rl.unload_texture(self._qr_texture)
class TermsPage(Scroller):
def __init__(self, on_accept, on_decline):
super().__init__()
def show_accept_dialog():
gui_app.push_widget(BigConfirmationDialogV2("accept\nterms", "icons_mici/setup/driver_monitoring/dm_check.png",
confirm_callback=on_accept))
def show_decline_dialog():
gui_app.push_widget(BigConfirmationDialogV2("decline &\nuninstall", "icons_mici/setup/cancel.png",
red=True, exit_on_confirm=False, confirm_callback=on_decline))
self._accept_button = BigCircleButton("icons_mici/setup/driver_monitoring/dm_check.png")
self._accept_button.set_click_callback(show_accept_dialog)
self._decline_button = BigCircleButton("icons_mici/setup/cancel.png", red=True)
self._decline_button.set_click_callback(show_decline_dialog)
self._scroller.add_widgets([
GreyBigButton("terms and\nconditions", "scroll to continue",
gui_app.texture("icons_mici/setup/green_info.png", 64, 64)),
GreyBigButton("swipe for QR code", "or go to https://sunnypilot.ai/terms",
gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 64, 56, flip_x=True)),
QRCodeWidget("https://sunnypilot.ai/terms"),
GreyBigButton("", "You must accept the Terms & Conditions to use sunnypilot."),
self._accept_button,
self._decline_button,
])
def _render(self, _): def _render(self, _):
rl.draw_rectangle_rec(self._rect, rl.BLACK) rl.draw_rectangle_rec(self._rect, rl.BLACK)
if self._step < len(self._steps): super()._render(_)
self._steps[self._step].render(self._rect)
class DeclinePage(Widget):
def __init__(self, back_callback=None):
super().__init__()
self._uninstall_slider = SmallSlider("uninstall sunnypilot", self._on_uninstall)
self._back_button = SmallButton("back")
self._back_button.set_click_callback(back_callback)
self._warning_header = TermsHeader("you must accept the\nterms to use sunnypilot",
gui_app.texture("icons_mici/setup/red_warning.png", 66, 60))
def _on_uninstall(self):
ui_state.params.put_bool("DoUninstall", True)
gui_app.request_close()
def _render(self, _):
self._warning_header.render(rl.Rectangle(
self._rect.x + 16,
self._rect.y + 16,
self._warning_header.rect.width,
self._warning_header.rect.height,
))
self._back_button.set_opacity(1 - self._uninstall_slider.slider_percentage)
self._back_button.render(rl.Rectangle(
self._rect.x + 8,
self._rect.y + self._rect.height - self._back_button.rect.height,
self._back_button.rect.width,
self._back_button.rect.height,
))
self._uninstall_slider.render(rl.Rectangle(
self._rect.x + self._rect.width - self._uninstall_slider.rect.width,
self._rect.y + self._rect.height - self._uninstall_slider.rect.height,
self._uninstall_slider.rect.width,
self._uninstall_slider.rect.height,
))
class TermsPage(SetupTermsPage):
def __init__(self, on_accept=None, on_decline=None):
super().__init__(on_accept, on_decline, "decline")
info_txt = gui_app.texture("icons_mici/setup/green_info.png", 60, 60)
self._title_header = TermsHeader("terms of service", info_txt)
self._terms_label = UnifiedLabel("You must accept the Terms of Service to use sunnypilot. " +
"Read the latest terms at https://sunnypilot.ai/terms before continuing.", 36,
FontWeight.ROMAN)
@property
def _content_height(self):
return self._terms_label.rect.y + self._terms_label.rect.height - self._scroll_panel.get_offset()
def _render_content(self, scroll_offset):
self._title_header.set_position(self._rect.x + 16, self._rect.y + 12 + scroll_offset)
self._title_header.render()
self._terms_label.render(rl.Rectangle(
self._rect.x + 16,
self._title_header.rect.y + self._title_header.rect.height + self.ITEM_SPACING,
self._rect.width - 100,
self._terms_label.get_content_height(int(self._rect.width - 100)),
))
class OnboardingWindow(Widget): class OnboardingWindow(Widget):
def __init__(self): def __init__(self, completed_callback: Callable[[], None]):
super().__init__() super().__init__()
self._accepted_terms: bool = ui_state.params.get("HasAcceptedTerms") == terms_version self._completed_callback = completed_callback
self._accepted_terms: bool = (ui_state.params.get("HasAcceptedTerms") == terms_version and
ui_state.params.get("HasAcceptedTermsSP") == terms_version_sp)
self._training_done: bool = ui_state.params.get("CompletedTrainingVersion") == training_version self._training_done: bool = ui_state.params.get("CompletedTrainingVersion") == training_version
self._sunnylink_consent_done: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") in {
sunnylink_consent_version, sunnylink_consent_declined
}
self._state = OnboardingState.TERMS if not self._accepted_terms else OnboardingState.ONBOARDING self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
self.set_rect(rl.Rectangle(0, 0, 458, gui_app.height)) # Windows — all pushed onto nav stack, _terms is always rendered as base layer
self._terms = TermsPage(on_accept=self._on_terms_accepted, on_decline=self._on_uninstall)
self._terms.set_enabled(lambda: self.enabled) # for nav stack
self._sunnylink_consent = SunnylinkConsentPage(
on_accept=self._on_sunnylink_accepted,
on_decline=self._on_sunnylink_declined,
)
# Windows
self._terms = TermsPage(on_accept=self._on_terms_accepted, on_decline=self._on_terms_declined)
self._training_guide = TrainingGuide(completed_callback=self._on_completed_training) self._training_guide = TrainingGuide(completed_callback=self._on_completed_training)
self._decline_page = DeclinePage(back_callback=self._on_decline_back) self._training_guide.set_enabled(lambda: self.enabled) # for nav stack
# sunnylink consent pages self._needs_initial_push = False
self._accepted_terms = self._accepted_terms and ui_state.params.get("HasAcceptedTermsSP") == terms_version_sp
self._sunnylink = SunnylinkOnboarding() def _on_uninstall(self):
if not self._accepted_terms: ui_state.params.put_bool("DoUninstall", True)
self._state = OnboardingState.TERMS
elif not self._sunnylink.completed:
self._state = OnboardingState.SUNNYLINK_CONSENT
elif not self._training_done:
self._state = OnboardingState.ONBOARDING
else:
self._state = OnboardingState.ONBOARDING
def show_event(self): def show_event(self):
super().show_event() super().show_event()
device.set_override_interactive_timeout(300) device.set_override_interactive_timeout(300)
device.set_offroad_brightness(100)
self._needs_initial_push = True
def hide_event(self): def hide_event(self):
super().hide_event() super().hide_event()
# FIXME: when nav stack sends hide event to widget 2 below on push, this needs to be moved
device.set_override_interactive_timeout(None) device.set_override_interactive_timeout(None)
device.set_offroad_brightness(None)
@property @property
def completed(self) -> bool: def completed(self) -> bool:
return self._accepted_terms and self._sunnylink.completed and self._training_done return self._accepted_terms and self._sunnylink_consent_done and self._training_done
def _on_terms_declined(self):
self._state = OnboardingState.DECLINE
def _on_decline_back(self):
self._state = OnboardingState.TERMS
def close(self): def close(self):
ui_state.params.put_bool("IsDriverViewEnabled", False) ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", False)
gui_app.pop_widget() self._completed_callback()
def _on_terms_accepted(self): def _on_terms_accepted(self):
ui_state.params.put("HasAcceptedTerms", terms_version) ui_state.params.put("HasAcceptedTerms", terms_version)
ui_state.params.put("HasAcceptedTermsSP", terms_version_sp) ui_state.params.put("HasAcceptedTermsSP", terms_version_sp)
if not self._sunnylink.completed: self._accepted_terms = True
self._state = OnboardingState.SUNNYLINK_CONSENT if not self._sunnylink_consent_done:
gui_app.push_widget(self._sunnylink_consent)
elif not self._training_done: elif not self._training_done:
self._state = OnboardingState.ONBOARDING gui_app.push_widget(self._training_guide)
else:
self.close()
def _on_sunnylink_accepted(self):
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
ui_state.params.put_bool("SunnylinkEnabled", True)
self._sunnylink_consent_done = True
if not self._training_done:
gui_app.push_widget(self._training_guide)
else:
self.close()
def _on_sunnylink_declined(self):
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_declined)
ui_state.params.put_bool("SunnylinkEnabled", False)
self._sunnylink_consent_done = True
if not self._training_done:
gui_app.push_widget(self._training_guide)
else: else:
self.close() self.close()
def _on_completed_training(self): def _on_completed_training(self):
ui_state.params.put("CompletedTrainingVersion", training_version) ui_state.params.put("CompletedTrainingVersion", training_version)
self._training_done = True
self.close() self.close()
def _render(self, _): def _render(self, _):
rl.draw_rectangle_rec(self._rect, rl.BLACK) rl.draw_rectangle_rec(self._rect, rl.BLACK)
if self._state == OnboardingState.TERMS:
self._terms.render(self._rect) # Deferred from show_event to avoid nested push_widget re-enable bug
elif self._state == OnboardingState.SUNNYLINK_CONSENT: if self._needs_initial_push:
self._sunnylink.render(self._rect) self._needs_initial_push = False
if self._sunnylink.completed: if self._accepted_terms and not self._sunnylink_consent_done:
if not self._training_done: gui_app.push_widget(self._sunnylink_consent)
self._state = OnboardingState.ONBOARDING elif self._accepted_terms and self._sunnylink_consent_done and not self._training_done:
else: gui_app.push_widget(self._training_guide)
self.close()
elif self._state == OnboardingState.ONBOARDING: self._terms.render(self._rect)
if not self._training_done:
self._training_guide.render(self._rect)
else:
self.close()
elif self._state == OnboardingState.DECLINE:
self._decline_page.render(self._rect)
+27 -3
View File
@@ -13,15 +13,39 @@ from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmatio
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage
from openpilot.system.ui.mici_setup import BigPillButton
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.system.ui.widgets.label import MiciLabel from openpilot.system.ui.widgets.label import MiciLabel
from openpilot.system.ui.widgets.html_render import HtmlModal, HtmlRenderer from openpilot.system.ui.widgets.html_render import HtmlModal, HtmlRenderer
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
class ReviewTermsPage(TermsPage, NavScroller):
"""TermsPage with NavWidget swipe-to-dismiss for reviewing in device settings."""
def __init__(self):
super().__init__(on_accept=self.dismiss, on_decline=self.dismiss)
self._accept_button.set_visible(False)
self._decline_button.set_visible(False)
close_button = BigPillButton("close")
close_button.set_click_callback(self.dismiss)
self._scroller.add_widget(close_button)
class ReviewTrainingGuide(TrainingGuide):
def show_event(self):
super().show_event()
device.set_override_interactive_timeout(300)
def hide_event(self):
super().hide_event()
device.set_override_interactive_timeout(None)
ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", False)
class MiciFccModal(NavRawScrollPanel): class MiciFccModal(NavRawScrollPanel):
def __init__(self, file_path: str | None = None, text: str | None = None): def __init__(self, file_path: str | None = None, text: str | None = None):
super().__init__() super().__init__()
@@ -311,11 +335,11 @@ class DeviceLayoutMici(NavScroller):
driver_cam_btn.set_enabled(lambda: ui_state.is_offroad()) driver_cam_btn.set_enabled(lambda: ui_state.is_offroad())
review_training_guide_btn = BigButton("review\ntraining guide", "", "icons_mici/settings/device/info.png") review_training_guide_btn = BigButton("review\ntraining guide", "", "icons_mici/settings/device/info.png")
review_training_guide_btn.set_click_callback(lambda: gui_app.push_widget(TrainingGuide(completed_callback=gui_app.pop_widget))) review_training_guide_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTrainingGuide(completed_callback=lambda: gui_app.pop_widgets_to(self))))
review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad()) review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad())
terms_btn = BigButton("terms &\nconditions", "", "icons_mici/settings/device/info.png") terms_btn = BigButton("terms &\nconditions", "", "icons_mici/settings/device/info.png")
terms_btn.set_click_callback(lambda: gui_app.push_widget(TermsPage(on_accept=gui_app.pop_widget))) terms_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTermsPage()))
terms_btn.set_enabled(lambda: ui_state.is_offroad()) terms_btn.set_enabled(lambda: ui_state.is_offroad())
self._scroller.add_widgets([ self._scroller.add_widgets([
@@ -1,13 +1,9 @@
import pyray as rl import pyray as rl
from openpilot.system.ui.widgets.scroller import NavScroller from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiIcon
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon from openpilot.selfdrive.ui.mici.widgets.button import BigButton
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigToggle
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType, ConnectStatus, SecurityType, normalize_ssid from openpilot.system.ui.lib.wifi_manager import WifiManager, ConnectStatus, SecurityType, normalize_ssid
class WifiNetworkButton(BigButton): class WifiNetworkButton(BigButton):
@@ -62,148 +58,3 @@ class WifiNetworkButton(BigButton):
lock_x = icon_x + self._txt_icon.width - self._lock_txt.width + 7 lock_x = icon_x + self._txt_icon.width - self._lock_txt.width + 7
lock_y = icon_y + self._txt_icon.height - self._lock_txt.height + 8 lock_y = icon_y + self._txt_icon.height - self._lock_txt.height + 8
rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, 1.0, rl.WHITE) rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, 1.0, rl.WHITE)
class NetworkLayoutMici(NavScroller):
def __init__(self):
super().__init__()
self._wifi_manager = WifiManager()
self._wifi_manager.set_active(False)
self._wifi_ui = WifiUIMici(self._wifi_manager)
self._wifi_manager.add_callbacks(
networks_updated=self._on_network_updated,
)
# ******** Tethering ********
def tethering_toggle_callback(checked: bool):
self._tethering_toggle_btn.set_enabled(False)
self._tethering_password_btn.set_enabled(False)
self._network_metered_btn.set_enabled(False)
self._wifi_manager.set_tethering_active(checked)
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
def tethering_password_callback(password: str):
if password:
self._tethering_toggle_btn.set_enabled(False)
self._tethering_password_btn.set_enabled(False)
self._wifi_manager.set_tethering_password(password)
def tethering_password_clicked():
tethering_password = self._wifi_manager.tethering_password
dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8,
confirm_callback=tethering_password_callback)
gui_app.push_widget(dlg)
txt_tethering = gui_app.texture("icons_mici/settings/network/tethering.png", 64, 54)
self._tethering_password_btn = BigButton("tethering password", "", txt_tethering)
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
# ******** Network Metered ********
def network_metered_callback(value: str):
self._network_metered_btn.set_enabled(False)
metered = {
'default': MeteredType.UNKNOWN,
'metered': MeteredType.YES,
'unmetered': MeteredType.NO
}.get(value, MeteredType.UNKNOWN)
self._wifi_manager.set_current_network_metered(metered)
# TODO: signal for current network metered type when changing networks, this is wrong until you press it once
# TODO: disable when not connected
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
self._network_metered_btn.set_enabled(False)
self._wifi_button = WifiNetworkButton(self._wifi_manager)
self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui))
# ******** Advanced settings ********
# ******** Roaming toggle ********
self._roaming_btn = BigParamControl("enable roaming", "GsmRoaming", toggle_callback=self._toggle_roaming)
# ******** APN settings ********
self._apn_btn = BigButton("apn settings", "edit")
self._apn_btn.set_click_callback(self._edit_apn)
# ******** Cellular metered toggle ********
self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered", toggle_callback=self._toggle_cellular_metered)
# Main scroller ----------------------------------
self._scroller.add_widgets([
self._wifi_button,
self._network_metered_btn,
self._tethering_toggle_btn,
self._tethering_password_btn,
# /* Advanced settings
self._roaming_btn,
self._apn_btn,
self._cellular_metered_btn,
# */
])
# Set initial config
roaming_enabled = ui_state.params.get_bool("GsmRoaming")
metered = ui_state.params.get_bool("GsmMetered")
self._wifi_manager.update_gsm_settings(roaming_enabled, ui_state.params.get("GsmApn") or "", metered)
def _update_state(self):
super()._update_state()
# If not using prime SIM, show GSM settings and enable IPv4 forwarding
show_cell_settings = ui_state.prime_state.get_type() in (PrimeType.NONE, PrimeType.LITE)
self._wifi_manager.set_ipv4_forward(show_cell_settings)
self._roaming_btn.set_visible(show_cell_settings)
self._apn_btn.set_visible(show_cell_settings)
self._cellular_metered_btn.set_visible(show_cell_settings)
def show_event(self):
super().show_event()
self._wifi_manager.set_active(True)
# Process wifi callbacks while at any point in the nav stack
gui_app.add_nav_stack_tick(self._wifi_manager.process_callbacks)
def hide_event(self):
super().hide_event()
self._wifi_manager.set_active(False)
gui_app.remove_nav_stack_tick(self._wifi_manager.process_callbacks)
def _toggle_roaming(self, checked: bool):
self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered"))
def _edit_apn(self):
def update_apn(apn: str):
apn = apn.strip()
if apn == "":
ui_state.params.remove("GsmApn")
else:
ui_state.params.put("GsmApn", apn)
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), apn, ui_state.params.get_bool("GsmMetered"))
current_apn = ui_state.params.get("GsmApn") or ""
dlg = BigInputDialog("enter APN...", current_apn, minimum_length=0, confirm_callback=update_apn)
gui_app.push_widget(dlg)
def _toggle_cellular_metered(self, checked: bool):
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), ui_state.params.get("GsmApn") or "", checked)
def _on_network_updated(self, networks: list[Network]):
# Update tethering state
tethering_active = self._wifi_manager.is_tethering_active()
# TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons
self._tethering_toggle_btn.set_enabled(True)
self._tethering_password_btn.set_enabled(True)
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
self._tethering_toggle_btn.set_checked(tethering_active)
# Update network metered
self._network_metered_btn.set_value(
{
MeteredType.UNKNOWN: 'default',
MeteredType.YES: 'metered',
MeteredType.NO: 'unmetered'
}.get(self._wifi_manager.current_network_metered, 'default'))
@@ -0,0 +1,154 @@
from openpilot.system.ui.widgets.scroller import NavScroller
from openpilot.selfdrive.ui.mici.layouts.settings.network import WifiNetworkButton
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigToggle
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType
class NetworkLayoutMici(NavScroller):
def __init__(self):
super().__init__()
self._wifi_manager = WifiManager()
self._wifi_manager.set_active(False)
self._wifi_ui = WifiUIMici(self._wifi_manager)
self._wifi_manager.add_callbacks(
networks_updated=self._on_network_updated,
)
# ******** Tethering ********
def tethering_toggle_callback(checked: bool):
self._tethering_toggle_btn.set_enabled(False)
self._tethering_password_btn.set_enabled(False)
self._network_metered_btn.set_enabled(False)
self._wifi_manager.set_tethering_active(checked)
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
def tethering_password_callback(password: str):
if password:
self._tethering_toggle_btn.set_enabled(False)
self._tethering_password_btn.set_enabled(False)
self._wifi_manager.set_tethering_password(password)
def tethering_password_clicked():
tethering_password = self._wifi_manager.tethering_password
dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8,
confirm_callback=tethering_password_callback)
gui_app.push_widget(dlg)
txt_tethering = gui_app.texture("icons_mici/settings/network/tethering.png", 64, 54)
self._tethering_password_btn = BigButton("tethering password", "", txt_tethering)
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
# ******** Network Metered ********
def network_metered_callback(value: str):
self._network_metered_btn.set_enabled(False)
metered = {
'default': MeteredType.UNKNOWN,
'metered': MeteredType.YES,
'unmetered': MeteredType.NO
}.get(value, MeteredType.UNKNOWN)
self._wifi_manager.set_current_network_metered(metered)
# TODO: signal for current network metered type when changing networks, this is wrong until you press it once
# TODO: disable when not connected
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
self._network_metered_btn.set_enabled(False)
self._wifi_button = WifiNetworkButton(self._wifi_manager)
self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui))
# ******** Advanced settings ********
# ******** Roaming toggle ********
self._roaming_btn = BigParamControl("enable roaming", "GsmRoaming", toggle_callback=self._toggle_roaming)
# ******** APN settings ********
self._apn_btn = BigButton("apn settings", "edit")
self._apn_btn.set_click_callback(self._edit_apn)
# ******** Cellular metered toggle ********
self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered", toggle_callback=self._toggle_cellular_metered)
# Main scroller ----------------------------------
self._scroller.add_widgets([
self._wifi_button,
self._network_metered_btn,
self._tethering_toggle_btn,
self._tethering_password_btn,
# /* Advanced settings
self._roaming_btn,
self._apn_btn,
self._cellular_metered_btn,
# */
])
# Set initial config
roaming_enabled = ui_state.params.get_bool("GsmRoaming")
metered = ui_state.params.get_bool("GsmMetered")
self._wifi_manager.update_gsm_settings(roaming_enabled, ui_state.params.get("GsmApn") or "", metered)
def _update_state(self):
super()._update_state()
# If not using prime SIM, show GSM settings and enable IPv4 forwarding
show_cell_settings = ui_state.prime_state.get_type() in (PrimeType.NONE, PrimeType.LITE)
self._wifi_manager.set_ipv4_forward(show_cell_settings)
self._roaming_btn.set_visible(show_cell_settings)
self._apn_btn.set_visible(show_cell_settings)
self._cellular_metered_btn.set_visible(show_cell_settings)
def show_event(self):
super().show_event()
self._wifi_manager.set_active(True)
# Process wifi callbacks while at any point in the nav stack
gui_app.add_nav_stack_tick(self._wifi_manager.process_callbacks)
def hide_event(self):
super().hide_event()
self._wifi_manager.set_active(False)
gui_app.remove_nav_stack_tick(self._wifi_manager.process_callbacks)
def _toggle_roaming(self, checked: bool):
self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered"))
def _edit_apn(self):
def update_apn(apn: str):
apn = apn.strip()
if apn == "":
ui_state.params.remove("GsmApn")
else:
ui_state.params.put("GsmApn", apn)
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), apn, ui_state.params.get_bool("GsmMetered"))
current_apn = ui_state.params.get("GsmApn") or ""
dlg = BigInputDialog("enter APN...", current_apn, minimum_length=0, confirm_callback=update_apn)
gui_app.push_widget(dlg)
def _toggle_cellular_metered(self, checked: bool):
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), ui_state.params.get("GsmApn") or "", checked)
def _on_network_updated(self, networks: list[Network]):
# Update tethering state
tethering_active = self._wifi_manager.is_tethering_active()
# TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons
self._tethering_toggle_btn.set_enabled(True)
self._tethering_password_btn.set_enabled(True)
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
self._tethering_toggle_btn.set_checked(tethering_active)
# Update network metered
self._network_metered_btn.set_value(
{
MeteredType.UNKNOWN: 'default',
MeteredType.YES: 'metered',
MeteredType.NO: 'unmetered'
}.get(self._wifi_manager.current_network_metered, 'default'))
@@ -2,7 +2,7 @@ from openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller import NavScroller from openpilot.system.ui.widgets.scroller import NavScroller
from openpilot.selfdrive.ui.mici.widgets.button import BigButton from openpilot.selfdrive.ui.mici.widgets.button import BigButton
from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.network import NetworkLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.network.network_layout import NetworkLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton
from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout
@@ -251,7 +251,7 @@ class AugmentedRoadView(CameraView):
# Draw darkened background and text if not onroad # Draw darkened background and text if not onroad
if not ui_state.started: if not ui_state.started:
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)) 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) self._offroad_label.render(self._rect)
# publish uiDebug # publish uiDebug
msg = messaging.new_message('uiDebug') msg = messaging.new_message('uiDebug')
+1 -1
View File
@@ -155,11 +155,11 @@ class CameraView(Widget):
# Prevent old frames from showing when going onroad. Qt has a separate thread # Prevent old frames from showing when going onroad. Qt has a separate thread
# which drains the VisionIpcClient SubSocket for us. Re-connecting is not enough # which drains the VisionIpcClient SubSocket for us. Re-connecting is not enough
# and only clears internal buffers, not the message queue. # and only clears internal buffers, not the message queue.
self.frame = None
self.available_streams.clear() self.available_streams.clear()
if self.client: if self.client:
del self.client del self.client
self.client = VisionIpcClient(self._name, self._stream_type, conflate=True) self.client = VisionIpcClient(self._name, self._stream_type, conflate=True)
self.frame = None
def _set_placeholder_color(self, color: rl.Color): def _set_placeholder_color(self, color: rl.Color):
"""Set a placeholder color to be drawn when no frame is available.""" """Set a placeholder color to be drawn when no frame is available."""
+1 -2
View File
@@ -172,8 +172,7 @@ class HudRenderer(Widget):
def _render(self, rect: rl.Rectangle) -> None: def _render(self, rect: rl.Rectangle) -> None:
"""Render HUD elements to the screen.""" """Render HUD elements to the screen."""
if ui_state.sm['controlsState'].lateralControlState.which() != 'angleState': self._torque_bar.render(rect)
self._torque_bar.render(rect)
if self.is_cruise_set: if self.is_cruise_set:
self._draw_set_speed(rect) self._draw_set_speed(rect)
+15 -5
View File
@@ -145,6 +145,9 @@ def arc_bar_pts(cx: float, cy: float,
return pts return pts
DEFAULT_MAX_LAT_ACCEL = 3.0 # m/s^2
class TorqueBar(Widget): class TorqueBar(Widget):
def __init__(self, demo: bool = False, scale: float = 1.0, always: bool = False): def __init__(self, demo: bool = False, scale: float = 1.0, always: bool = False):
super().__init__() super().__init__()
@@ -167,16 +170,23 @@ class TorqueBar(Widget):
controls_state = ui_state.sm['controlsState'] controls_state = ui_state.sm['controlsState']
car_state = ui_state.sm['carState'] car_state = ui_state.sm['carState']
live_parameters = ui_state.sm['liveParameters'] live_parameters = ui_state.sm['liveParameters']
lateral_acceleration = controls_state.curvature * car_state.vEgo ** 2 - live_parameters.roll * ACCELERATION_DUE_TO_GRAVITY car_control = ui_state.sm['carControl']
# TODO: pull from carparams
max_lateral_acceleration = 3
# from selfdrived # Include lateral accel error in estimated torque utilization
actual_lateral_accel = controls_state.curvature * car_state.vEgo ** 2 actual_lateral_accel = controls_state.curvature * car_state.vEgo ** 2
desired_lateral_accel = controls_state.desiredCurvature * car_state.vEgo ** 2 desired_lateral_accel = controls_state.desiredCurvature * car_state.vEgo ** 2
accel_diff = (desired_lateral_accel - actual_lateral_accel) accel_diff = (desired_lateral_accel - actual_lateral_accel)
self._torque_filter.update(min(max(lateral_acceleration / max_lateral_acceleration + accel_diff, -1), 1)) # Include road roll in estimated torque utilization
# Roll is less accurate near standstill, so reduce its effect at low speed
roll_compensation = live_parameters.roll * ACCELERATION_DUE_TO_GRAVITY * np.interp(car_state.vEgo, [5, 15], [0.0, 1.0])
lateral_acceleration = actual_lateral_accel - roll_compensation
max_lateral_acceleration = ui_state.CP.maxLateralAccel if ui_state.CP else DEFAULT_MAX_LAT_ACCEL
if not car_control.latActive:
self._torque_filter.update(0.0)
else:
self._torque_filter.update(np.clip((lateral_acceleration + accel_diff) / max_lateral_acceleration, -1, 1))
else: else:
self._torque_filter.update(-ui_state.sm['carOutput'].actuatorsOutput.torque) self._torque_filter.update(-ui_state.sm['carOutput'].actuatorsOutput.torque)
+3 -1
View File
@@ -68,7 +68,9 @@ def test_dialogs_do_not_leak():
for ctor in ( for ctor in (
# mici # mici
MiciDriverCameraDialog, MiciTrainingGuide, MiciOnboardingWindow, MiciPairingDialog, MiciDriverCameraDialog, MiciPairingDialog,
lambda: MiciTrainingGuide(lambda: None),
lambda: MiciOnboardingWindow(lambda: None),
lambda: BigDialog("test", "test"), lambda: BigDialog("test", "test"),
lambda: BigConfirmationDialogV2("test", "icons_mici/settings/network/new/trash.png"), lambda: BigConfirmationDialogV2("test", "icons_mici/settings/network/new/trash.png"),
lambda: BigInputDialog("test"), lambda: BigInputDialog("test"),
+12 -1
View File
@@ -120,6 +120,7 @@ class BigButton(Widget):
self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps) self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps)
self._click_delay = 0.075 self._click_delay = 0.075
self._shake_start: float | None = None self._shake_start: float | None = None
self._grow_animation_until: float | None = None
self._rotate_icon_t: float | None = None self._rotate_icon_t: float | None = None
@@ -145,6 +146,9 @@ class BigButton(Widget):
self._txt_pressed_bg = gui_app.texture("icons_mici/buttons/button_rectangle_pressed.png", 402, 180) self._txt_pressed_bg = gui_app.texture("icons_mici/buttons/button_rectangle_pressed.png", 402, 180)
self._txt_disabled_bg = gui_app.texture("icons_mici/buttons/button_rectangle_disabled.png", 402, 180) self._txt_disabled_bg = gui_app.texture("icons_mici/buttons/button_rectangle_disabled.png", 402, 180)
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
super().set_touch_valid_callback(lambda: touch_callback() and self._grow_animation_until is None)
def _width_hint(self) -> int: def _width_hint(self) -> int:
# Single line if scrolling, so hide behind icon if exists # Single line if scrolling, so hide behind icon if exists
icon_size = self._icon_size[0] if self._txt_icon and self._scroll and self.value else 0 icon_size = self._icon_size[0] if self._txt_icon and self._scroll and self.value else 0
@@ -182,6 +186,9 @@ class BigButton(Widget):
def trigger_shake(self): def trigger_shake(self):
self._shake_start = rl.get_time() self._shake_start = rl.get_time()
def trigger_grow_animation(self, duration: float = 0.65):
self._grow_animation_until = rl.get_time() + duration
@property @property
def _shake_offset(self) -> float: def _shake_offset(self) -> float:
SHAKE_DURATION = 0.5 SHAKE_DURATION = 0.5
@@ -197,6 +204,10 @@ class BigButton(Widget):
super().set_position(x + self._shake_offset, y) super().set_position(x + self._shake_offset, y)
def _handle_background(self) -> tuple[rl.Texture, float, float, float]: def _handle_background(self) -> tuple[rl.Texture, float, float, float]:
if self._grow_animation_until is not None:
if rl.get_time() >= self._grow_animation_until:
self._grow_animation_until = None
# draw _txt_default_bg # draw _txt_default_bg
txt_bg = self._txt_default_bg txt_bg = self._txt_default_bg
if not self.enabled: if not self.enabled:
@@ -204,7 +215,7 @@ class BigButton(Widget):
elif self.is_pressed: elif self.is_pressed:
txt_bg = self._txt_pressed_bg txt_bg = self._txt_pressed_bg
scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed else 1.0) scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed or self._grow_animation_until is not None else 1.0)
btn_x = self._rect.x + (self._rect.width * (1 - scale)) / 2 btn_x = self._rect.x + (self._rect.width * (1 - scale)) / 2
btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2 btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2
return txt_bg, btn_x, btn_y, scale return txt_bg, btn_x, btn_y, scale
+3 -2
View File
@@ -103,11 +103,12 @@ class BigInputDialog(BigDialogBase):
hint: str, hint: str,
default_text: str = "", default_text: str = "",
minimum_length: int = 1, minimum_length: int = 1,
confirm_callback: Callable[[str], None] | None = None): confirm_callback: Callable[[str], None] | None = None,
auto_return_to_letters: str = ""):
super().__init__() super().__init__()
self._hint_label = UnifiedLabel(hint, font_size=35, text_color=rl.Color(255, 255, 255, int(255 * 0.35)), self._hint_label = UnifiedLabel(hint, font_size=35, text_color=rl.Color(255, 255, 255, int(255 * 0.35)),
font_weight=FontWeight.MEDIUM) font_weight=FontWeight.MEDIUM)
self._keyboard = MiciKeyboard() self._keyboard = MiciKeyboard(auto_return_to_letters=auto_return_to_letters)
self._keyboard.set_text(default_text) self._keyboard.set_text(default_text)
self._keyboard.set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack + NavWidget self._keyboard.set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack + NavWidget
self._minimum_length = minimum_length self._minimum_length = minimum_length
@@ -4,90 +4,37 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License. This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details. See the LICENSE.md file in the root directory for more details.
""" """
import pyray as rl from collections.abc import Callable
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton
from openpilot.system.ui.widgets.slider import SmallSlider from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialogV2
from openpilot.system.ui.mici_setup import TermsHeader, TermsPage as SetupTermsPage from openpilot.system.ui.lib.application import gui_app
from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined from openpilot.system.ui.mici_setup import GreyBigButton
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.widgets.scroller import NavScroller
class SunnylinkConsentPage(SetupTermsPage): class SunnylinkConsentPage(NavScroller):
def __init__(self, on_accept=None, on_decline=None, left_text: str = "disable", right_text: str = "enable"): def __init__(self, on_accept: Callable | None = None, on_decline: Callable | None = None):
super().__init__(on_accept, on_decline, left_text, continue_text=right_text) super().__init__()
self._title_header = TermsHeader("sunnylink", def show_accept_dialog():
gui_app.texture("../../sunnypilot/selfdrive/assets/logo.png", 66, 60)) gui_app.push_widget(BigConfirmationDialogV2("enable\nsunnylink", "icons_mici/setup/driver_monitoring/dm_check.png",
confirm_callback=on_accept))
self._terms_label = UnifiedLabel("sunnylink enables secured remote access to your comma device from anywhere, " + def show_decline_dialog():
"including settings management, remote monitoring, real-time dashboard, etc.", gui_app.push_widget(BigConfirmationDialogV2("disable\nsunnylink", "icons_mici/setup/cancel.png",
36, FontWeight.ROMAN) red=True, confirm_callback=on_decline))
@property self._accept_button = BigCircleButton("icons_mici/setup/driver_monitoring/dm_check.png")
def _content_height(self): self._accept_button.set_click_callback(show_accept_dialog)
return self._terms_label.rect.y + self._terms_label.rect.height - self._scroll_panel.get_offset()
def _render_content(self, scroll_offset): self._decline_button = BigCircleButton("icons_mici/setup/cancel.png", red=True)
self._title_header.set_position(self._rect.x + 16, self._rect.y + 12 + scroll_offset) self._decline_button.set_click_callback(show_decline_dialog)
self._title_header.render()
self._terms_label.render(rl.Rectangle( self._scroller.add_widgets([
self._rect.x + 16, GreyBigButton("sunnylink", "scroll to continue",
self._title_header.rect.y + self._title_header.rect.height + self.ITEM_SPACING, gui_app.texture("../../sunnypilot/selfdrive/assets/logo.png", 64, 64)),
self._rect.width - 100, GreyBigButton("", "sunnylink enables secured remote access to your comma device from anywhere."),
self._terms_label.get_content_height(int(self._rect.width - 100)), self._accept_button,
)) self._decline_button,
])
class SunnylinkConsentDisableConfirmPage(SunnylinkConsentPage):
def __init__(self, on_accept=None, on_decline=None):
super().__init__(on_accept=on_decline, on_decline=on_accept, left_text="enable", right_text="disable")
# we flip the continue & disable buttons to use slider for disable
self._continue_slider = True
self._continue_button = SmallSlider("disable", confirm_callback=on_decline)
self._scroll_panel.set_enabled(lambda: not self._continue_button.is_pressed)
self._title_header = TermsHeader("disable sunnylink?",
gui_app.texture("icons_mici/setup/red_warning.png", 66, 60))
self._terms_label = UnifiedLabel("sunnylink is designed to be enabled as part of sunnypilot's core functionality. " +
"If sunnylink is disabled, features such as settings management, " +
"remote monitoring, real-time dashboards will be unavailable.",
36, FontWeight.ROMAN)
class SunnylinkOnboarding:
def __init__(self):
self.consent_done: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") in {sunnylink_consent_version, sunnylink_consent_declined}
self.disable_confirm = False
self.consent_page = SunnylinkConsentPage(on_decline=self._on_decline, on_accept=self._on_accept)
self.confirm_page = SunnylinkConsentDisableConfirmPage(on_decline=self._on_confirm_decline, on_accept=self._on_accept)
@property
def completed(self) -> bool:
return self.consent_done
def _on_accept(self):
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
ui_state.params.put_bool("SunnylinkEnabled", True)
self.consent_done = True
def _on_decline(self):
self.disable_confirm = True
def _on_confirm_decline(self):
ui_state.params.put_bool("SunnylinkEnabled", False)
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_declined)
self.consent_done = True
def render(self, rect):
if self.consent_done:
return
if self.disable_confirm:
self.confirm_page.render(rect)
else:
self.consent_page.render(rect)
@@ -23,7 +23,7 @@ class AugmentedRoadViewSP:
def update_fade_out_bottom_overlay(self, _content_rect): def update_fade_out_bottom_overlay(self, _content_rect):
# Fade out bottom of overlays for looks (only when engaged) # Fade out bottom of overlays for looks (only when engaged)
fade_alpha = self._fade_alpha_filter.update(ui_state.status != UIStatus.DISENGAGED) fade_alpha = self._fade_alpha_filter.update(ui_state.status != UIStatus.DISENGAGED)
if ui_state.torque_bar and ui_state.sm['controlsState'].lateralControlState.which() != 'angleState' and fade_alpha > 1e-2: if ui_state.torque_bar and fade_alpha > 1e-2:
# Scale the fade texture to the content rect # Scale the fade texture to the content rect
rl.draw_texture_pro(self._fade_texture, rl.draw_texture_pro(self._fade_texture,
rl.Rectangle(0, 0, self._fade_texture.width, self._fade_texture.height), rl.Rectangle(0, 0, self._fade_texture.width, self._fade_texture.height),
@@ -131,7 +131,7 @@ class HudRendererSP(HudRenderer):
def _render(self, rect: rl.Rectangle) -> None: def _render(self, rect: rl.Rectangle) -> None:
super()._render(rect) super()._render(rect)
if ui_state.torque_bar and ui_state.sm['controlsState'].lateralControlState.which() != 'angleState': if ui_state.torque_bar:
torque_rect = rect torque_rect = rect
if ui_state.developer_ui in (DeveloperUiState.BOTTOM, DeveloperUiState.BOTH): if ui_state.developer_ui in (DeveloperUiState.BOTTOM, DeveloperUiState.BOTH):
torque_rect = rl.Rectangle(rect.x, rect.y, rect.width, rect.height - get_bottom_dev_ui_offset()) torque_rect = rl.Rectangle(rect.x, rect.y, rect.width, rect.height - get_bottom_dev_ui_offset())
@@ -72,9 +72,10 @@ def _flash_panda(panda: Panda) -> None:
_flash_static(panda._handle, code) _flash_static(panda._handle, code)
panda.reconnect() panda.reconnect()
cloudlog.info(f"Successfully flashed xnor's Rivian Longitudinal Upgrade Kit: {panda.get_usb_serial()}")
def flash_rivian_long(panda: Panda) -> None: def flash_rivian_long(panda_serials: list[str]) -> None:
if not os.path.isfile(FW_PATH): if not os.path.isfile(FW_PATH):
cloudlog.error(f"Rivian longitudinal upgrade firmware not found at {FW_PATH}") cloudlog.error(f"Rivian longitudinal upgrade firmware not found at {FW_PATH}")
return return
@@ -83,13 +84,18 @@ def flash_rivian_long(panda: Panda) -> None:
cloudlog.info("Not a Rivian, skipping longitudinal upgrade...") cloudlog.info("Not a Rivian, skipping longitudinal upgrade...")
return return
# only flash external black pandas (HW_TYPE_BLACK = 0x03) for serial in panda_serials:
if panda.get_type() == b'\x03' and not panda.is_internal(): panda = Panda(serial)
try: # only flash external black pandas (HW_TYPE_BLACK = 0x03)
_flash_panda(panda) if panda.get_type() == b'\x03' and not panda.is_internal():
except Exception: try:
cloudlog.exception(f"Failed to flash F4 panda {panda.get_usb_serial()}") _flash_panda(panda)
except Exception:
cloudlog.exception(f"Failed to flash xnor's Rivian Longitudinal Upgrade Kit: {serial}")
panda.close()
return
if __name__ == '__main__': if __name__ == '__main__':
flash_rivian_long(Panda()) flash_rivian_long(Panda.list())
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c44fb88b3b1643b6b44ae8ac9880348bd0257ff90f4084cbe889de91d71653fe oid sha256:d815a9140e69242d85e57f74a14c6372809eebefe8958be9152efa7874928ccc
size 25111329 size 71215510
+3
View File
@@ -436,6 +436,9 @@ class GuiApplication(GuiApplicationExt):
return self._nav_stack[-1] return self._nav_stack[-1]
return None return None
def widget_in_stack(self, widget: object) -> bool:
return widget in self._nav_stack
def add_nav_stack_tick(self, tick_function: Callable[[], None]): def add_nav_stack_tick(self, tick_function: Callable[[], None]):
if tick_function not in self._nav_stack_ticks: if tick_function not in self._nav_stack_ticks:
self._nav_stack_ticks.append(tick_function) self._nav_stack_ticks.append(tick_function)
+6 -6
View File
@@ -1,12 +1,13 @@
import io import io
import re import re
import functools
from importlib.resources import as_file
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import FONT_DIR from openpilot.system.ui.lib.application import FONT_DIR
_emoji_font: ImageFont.FreeTypeFont | None = None
_cache: dict[str, rl.Texture] = {} _cache: dict[str, rl.Texture] = {}
EMOJI_REGEX = re.compile( EMOJI_REGEX = re.compile(
@@ -33,11 +34,10 @@ EMOJI_REGEX = re.compile(
flags=re.UNICODE flags=re.UNICODE
) )
def _load_emoji_font() -> ImageFont.FreeTypeFont | None: @functools.cache
global _emoji_font def _load_emoji_font() -> ImageFont.FreeTypeFont:
if _emoji_font is None: with as_file(FONT_DIR.joinpath("NotoColorEmoji.ttf")) as font_path:
_emoji_font = ImageFont.truetype(str(FONT_DIR.joinpath("NotoColorEmoji.ttf")), 109) return ImageFont.truetype(io.BytesIO(font_path.read_bytes()), 109)
return _emoji_font
def find_emoji(text): def find_emoji(text):
return [(m.start(), m.end(), m.group()) for m in EMOJI_REGEX.finditer(text)] return [(m.start(), m.end(), m.group()) for m in EMOJI_REGEX.finditer(text)]
+1 -1
View File
@@ -74,7 +74,7 @@ def load_translations(path) -> tuple[dict[str, str], dict[str, list[str]]]:
translations: msgid -> msgstr translations: msgid -> msgstr
plurals: msgid -> [msgstr[0], msgstr[1], ...] plurals: msgid -> [msgstr[0], msgstr[1], ...]
""" """
with open(str(path), encoding='utf-8') as f: with path.open(encoding='utf-8') as f:
lines = f.readlines() lines = f.readlines()
translations: dict[str, str] = {} translations: dict[str, str] = {}
+1
View File
@@ -91,6 +91,7 @@ class Reset(Widget):
if self._mode == ResetMode.RECOVER: if self._mode == ResetMode.RECOVER:
self._cancel_button.set_text("reboot") self._cancel_button.set_text("reboot")
self._cancel_button.set_click_callback(self._do_reboot)
self._cancel_button.render(rl.Rectangle( self._cancel_button.render(rl.Rectangle(
rect.x + 8, rect.x + 8,
rect.y + rect.height - self._cancel_button.rect.height, rect.y + rect.height - self._cancel_button.rect.height,
+188 -270
View File
@@ -1,5 +1,4 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from abc import abstractmethod
import os import os
import re import re
import threading import threading
@@ -14,21 +13,22 @@ import pyray as rl
from cereal import log from cereal import log
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.system.hardware import HARDWARE, TICI
from openpilot.common.realtime import config_realtime_process, set_core_affinity from openpilot.common.realtime import config_realtime_process, set_core_affinity
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.common.utils import run_cmd from openpilot.common.utils import run_cmd
from openpilot.system.hardware import HARDWARE, TICI
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.lib.wifi_manager import WifiManager
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.widgets.nav_widget import NavWidget
from openpilot.system.ui.widgets.button import (IconButton, SmallButton, WideRoundedButton, SmallerRoundedButton, from openpilot.system.ui.widgets.button import SmallButton
SmallCircleIconButton, WidishRoundedButton, FullRoundedButton)
from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.scroller import Scroller, NavScroller, ITEM_SPACING
from openpilot.system.ui.widgets.slider import LargerSlider, SmallSlider from openpilot.system.ui.widgets.slider import LargerSlider, SmallSlider
from openpilot.selfdrive.ui.mici.layouts.settings.network import WifiUIMici from openpilot.selfdrive.ui.mici.layouts.settings.network import WifiNetworkButton
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
NetworkType = log.DeviceState.NetworkType NetworkType = log.DeviceState.NetworkType
@@ -122,9 +122,9 @@ class SoftwareSelectionPage(NavWidget):
use_custom_software_callback: Callable): use_custom_software_callback: Callable):
super().__init__() super().__init__()
self._openpilot_slider = LargerSlider("slide to use\nopenpilot", use_openpilot_callback) self._openpilot_slider = LargerSlider("slide to install\nopenpilot", use_openpilot_callback)
self._openpilot_slider.set_enabled(lambda: self.enabled and not self.is_dismissing) self._openpilot_slider.set_enabled(lambda: self.enabled and not self.is_dismissing)
self._custom_software_slider = LargerSlider("slide to use\ncustom software", use_custom_software_callback, green=False) self._custom_software_slider = LargerSlider("slide to install\nother software", use_custom_software_callback, green=False)
self._custom_software_slider.set_enabled(lambda: self.enabled and not self.is_dismissing) self._custom_software_slider.set_enabled(lambda: self.enabled and not self.is_dismissing)
def show_event(self): def show_event(self):
@@ -161,199 +161,24 @@ class SoftwareSelectionPage(NavWidget):
self._custom_software_slider.render(custom_software_rect) self._custom_software_slider.render(custom_software_rect)
class TermsHeader(Widget): class CustomSoftwareWarningPage(NavScroller):
def __init__(self, text: str, icon_texture: rl.Texture):
super().__init__()
self._title = UnifiedLabel(text, 36, text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
font_weight=FontWeight.BOLD, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
line_height=0.8)
self._icon_texture = icon_texture
self.set_rect(rl.Rectangle(0, 0, gui_app.width - 16 * 2, self._icon_texture.height))
def set_title(self, text: str):
self._title.set_text(text)
def set_icon(self, icon_texture: rl.Texture):
self._icon_texture = icon_texture
def _render(self, _):
rl.draw_texture_ex(self._icon_texture, rl.Vector2(self._rect.x, self._rect.y),
0.0, 1.0, rl.WHITE)
# May expand outside parent rect
title_content_height = self._title.get_content_height(int(self._rect.width - self._icon_texture.width - 16))
title_rect = rl.Rectangle(
self._rect.x + self._icon_texture.width + 16,
self._rect.y + (self._rect.height - title_content_height) / 2,
self._rect.width - self._icon_texture.width - 16,
title_content_height,
)
self._title.render(title_rect)
class TermsPage(Widget):
ITEM_SPACING = 20
def __init__(self, continue_callback: Callable, back_callback: Callable | None = None,
back_text: str = "back", continue_text: str = "accept"):
super().__init__()
# TODO: use Scroller
self._scroll_panel = GuiScrollPanel2(horizontal=False)
self._continue_text = continue_text
self._continue_slider: bool = continue_text in ("reboot", "power off")
self._continue_button: WideRoundedButton | FullRoundedButton | SmallSlider
if self._continue_slider:
self._continue_button = SmallSlider(continue_text, confirm_callback=continue_callback)
self._scroll_panel.set_enabled(lambda: not self._continue_button.is_pressed)
elif back_callback is not None:
self._continue_button = WideRoundedButton(continue_text)
else:
self._continue_button = FullRoundedButton(continue_text)
self._continue_button.set_enabled(False)
self._continue_button.set_opacity(0.0)
self._continue_button.set_touch_valid_callback(self._scroll_panel.is_touch_valid)
if not self._continue_slider:
self._continue_button.set_click_callback(continue_callback)
self._enable_back = back_callback is not None
self._back_button = SmallButton(back_text)
self._back_button.set_opacity(0.0)
self._back_button.set_touch_valid_callback(self._scroll_panel.is_touch_valid)
self._back_button.set_click_callback(back_callback)
self._scroll_down_indicator = IconButton(gui_app.texture("icons_mici/setup/scroll_down_indicator.png", 64, 78))
self._scroll_down_indicator.set_enabled(False)
def reset(self):
self._scroll_panel.set_offset(0)
self._continue_button.set_enabled(False)
self._continue_button.set_opacity(0.0)
self._back_button.set_enabled(False)
self._back_button.set_opacity(0.0)
self._scroll_down_indicator.set_opacity(1.0)
def show_event(self):
super().show_event()
self.reset()
@property
@abstractmethod
def _content_height(self):
pass
@property
def _scrolled_down_offset(self):
return -self._content_height + (self._continue_button.rect.height + 16 + 30)
@abstractmethod
def _render_content(self, scroll_offset):
pass
def _render(self, _):
rl.draw_rectangle_rec(self._rect, rl.BLACK)
scroll_offset = round(self._scroll_panel.update(self._rect, self._content_height + self._continue_button.rect.height + 16))
if scroll_offset <= self._scrolled_down_offset:
# don't show back if not enabled
if self._enable_back:
self._back_button.set_enabled(True)
self._back_button.set_opacity(1.0, smooth=True)
self._continue_button.set_enabled(True)
self._continue_button.set_opacity(1.0, smooth=True)
self._scroll_down_indicator.set_opacity(0.0, smooth=True)
else:
self._back_button.set_enabled(False)
self._back_button.set_opacity(0.0, smooth=True)
self._continue_button.set_enabled(False)
self._continue_button.set_opacity(0.0, smooth=True)
self._scroll_down_indicator.set_opacity(1.0, smooth=True)
# Render content
self._render_content(scroll_offset)
# black gradient at top and bottom for scrolling content
rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y),
int(self._rect.width), 20, rl.BLACK, rl.BLANK)
rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - 20),
int(self._rect.width), 20, rl.BLANK, rl.BLACK)
# fade out back button as slider is moved
if self._continue_slider and scroll_offset <= self._scrolled_down_offset:
self._back_button.set_opacity(1.0 - self._continue_button.slider_percentage)
self._back_button.set_visible(self._continue_button.slider_percentage < 0.99)
self._back_button.render(rl.Rectangle(
self._rect.x + 8,
self._rect.y + self._rect.height - self._back_button.rect.height,
self._back_button.rect.width,
self._back_button.rect.height,
))
continue_x = self._rect.x + 8
if self._enable_back:
continue_x = self._rect.x + self._rect.width - self._continue_button.rect.width - 8
if self._continue_slider:
continue_x += 8
self._continue_button.render(rl.Rectangle(
continue_x,
self._rect.y + self._rect.height - self._continue_button.rect.height,
self._continue_button.rect.width,
self._continue_button.rect.height,
))
self._scroll_down_indicator.render(rl.Rectangle(
self._rect.x + self._rect.width - self._scroll_down_indicator.rect.width - 8,
self._rect.y + self._rect.height - self._scroll_down_indicator.rect.height - 8,
self._scroll_down_indicator.rect.width,
self._scroll_down_indicator.rect.height,
))
class CustomSoftwareWarningPage(TermsPage):
def __init__(self, continue_callback: Callable, back_callback: Callable): def __init__(self, continue_callback: Callable, back_callback: Callable):
super().__init__(continue_callback, back_callback) super().__init__()
self.set_back_callback(back_callback)
self._title_header = TermsHeader("use caution installing\n3rd party software", self._continue_button = BigPillButton("next")
gui_app.texture("icons_mici/setup/warning.png", 66, 60)) self._continue_button.set_click_callback(continue_callback)
self._body = UnifiedLabel("• It has not been tested by comma.\n" +
"• It may not comply with relevant safety standards.\n" +
"• It may cause damage to your device and/or vehicle.\n", 36, text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
font_weight=FontWeight.ROMAN)
self._restore_header = TermsHeader("how to backup &\nrestore", gui_app.texture("icons_mici/setup/restore.png", 60, 60)) self._scroller.add_widgets([
self._restore_body = UnifiedLabel("To restore your device to a factory state later, use https://flash.comma.ai", GreyBigButton("use caution", "when installing\n3rd party software",
36, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), gui_app.texture("icons_mici/setup/warning.png", 64, 58)),
font_weight=FontWeight.ROMAN) GreyBigButton("", "• It has not been tested by comma"),
GreyBigButton("", "• It may not comply with relevant safety standards."),
@property GreyBigButton("", "• It may cause damage to your device and/or vehicle."),
def _content_height(self): GreyBigButton("how to restore to a\nfactory state later", "https://flash.comma.ai",
return self._restore_body.rect.y + self._restore_body.rect.height - self._scroll_panel.get_offset() gui_app.texture("icons_mici/setup/restore.png", 64, 64)),
self._continue_button,
def _render_content(self, scroll_offset): ])
self._title_header.set_position(self._rect.x + 16, self._rect.y + 8 + scroll_offset)
self._title_header.render()
body_rect = rl.Rectangle(
self._rect.x + 8,
self._title_header.rect.y + self._title_header.rect.height + self.ITEM_SPACING,
self._rect.width - 50,
self._body.get_content_height(int(self._rect.width - 50)),
)
self._body.render(body_rect)
self._restore_header.set_position(self._rect.x + 16, self._body.rect.y + self._body.rect.height + self.ITEM_SPACING)
self._restore_header.render()
self._restore_body.render(rl.Rectangle(
self._rect.x + 8,
self._restore_header.rect.y + self._restore_header.rect.height + self.ITEM_SPACING,
self._rect.width - 50,
self._restore_body.get_content_height(int(self._rect.width - 50)),
))
class DownloadingPage(Widget): class DownloadingPage(Widget):
@@ -391,11 +216,9 @@ class DownloadingPage(Widget):
)) ))
class FailedPage(NavWidget): class FailedPageBase(Widget):
def __init__(self, reboot_callback: Callable, retry_callback: Callable, title: str = "download failed"): def __init__(self, reboot_callback: Callable, retry_callback: Callable, title: str = "download failed"):
super().__init__() super().__init__()
self.set_back_callback(retry_callback)
self._title_label = UnifiedLabel(title, 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), self._title_label = UnifiedLabel(title, 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
font_weight=FontWeight.DISPLAY) font_weight=FontWeight.DISPLAY)
self._reason_label = UnifiedLabel("", 36, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)), self._reason_label = UnifiedLabel("", 36, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)),
@@ -446,11 +269,86 @@ class FailedPage(NavWidget):
)) ))
class NetworkSetupPage(NavWidget): class FailedPage(FailedPageBase, NavWidget):
def __init__(self, reboot_callback: Callable, retry_callback: Callable, title: str = "download failed"):
super().__init__(reboot_callback, retry_callback, title)
self.set_back_callback(retry_callback)
class GreyBigButton(BigButton):
"""Users should manage newlines with this class themselves"""
LABEL_HORIZONTAL_PADDING = 30
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_touch_valid_callback(lambda: False)
self._rect.width = 476
self._label.set_font_size(36)
self._label.set_font_weight(FontWeight.BOLD)
self._label.set_line_height(1.0)
self._sub_label.set_font_size(36)
self._sub_label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.9)))
self._sub_label.set_font_weight(FontWeight.DISPLAY_REGULAR)
self._sub_label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE if not self._label.text else
rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
self._sub_label.set_line_height(0.95)
@property
def LABEL_VERTICAL_PADDING(self):
return BigButton.LABEL_VERTICAL_PADDING if self._label.text else 18
def _width_hint(self) -> int:
return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2)
def _render(self, _):
rl.draw_rectangle_rounded(self._rect, 0.4, 10, rl.Color(255, 255, 255, int(255 * 0.15)))
self._draw_content(self._rect.y)
class BigPillButton(BigButton):
def __init__(self, *args, green: bool = False, disabled_background: bool = False, **kwargs):
self._green = green
self._disabled_background = disabled_background
super().__init__(*args, **kwargs)
self._label.set_font_size(48)
self._label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
def _load_images(self):
if self._green:
self._txt_default_bg = gui_app.texture("icons_mici/setup/start_button.png", 402, 180)
self._txt_pressed_bg = gui_app.texture("icons_mici/setup/start_button_pressed.png", 402, 180)
else:
self._txt_default_bg = gui_app.texture("icons_mici/setup/continue.png", 402, 180)
self._txt_pressed_bg = gui_app.texture("icons_mici/setup/continue_pressed.png", 402, 180)
self._txt_disabled_bg = gui_app.texture("icons_mici/setup/continue_disabled.png", 402, 180)
def set_green(self, green: bool):
if self._green != green:
self._green = green
self._load_images()
def _update_label_layout(self):
# Don't change label text size
pass
def _handle_background(self) -> tuple[rl.Texture, float, float, float]:
txt_bg, btn_x, btn_y, scale = super()._handle_background()
if self._disabled_background:
txt_bg = self._txt_disabled_bg
return txt_bg, btn_x, btn_y, scale
class NetworkSetupPageBase(Scroller):
def __init__(self, network_monitor: NetworkConnectivityMonitor, continue_callback: Callable[[bool], None], def __init__(self, network_monitor: NetworkConnectivityMonitor, continue_callback: Callable[[bool], None],
back_callback: Callable[[], None] | None): disable_connect_hint: bool = False):
super().__init__() super().__init__()
self.set_back_callback(back_callback)
self._wifi_manager = WifiManager() self._wifi_manager = WifiManager()
self._wifi_manager.set_active(True) self._wifi_manager.set_active(True)
@@ -459,83 +357,106 @@ class NetworkSetupPage(NavWidget):
self._prev_has_internet = False self._prev_has_internet = False
self._wifi_ui = WifiUIMici(self._wifi_manager) self._wifi_ui = WifiUIMici(self._wifi_manager)
self._no_wifi_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 58, 50) self._connect_button = GreyBigButton("connect to\ninternet", "swipe down to go back",
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 58, 50) gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 64, 56, flip_x=True))
self._waiting_text = "waiting for internet..." self._connect_button.set_visible(not disable_connect_hint)
self._network_header = TermsHeader(self._waiting_text, self._no_wifi_txt)
back_txt = gui_app.texture("icons_mici/setup/back_new.png", 37, 32) self._wifi_button = WifiNetworkButton(self._wifi_manager)
self._back_button = SmallCircleIconButton(back_txt)
self._back_button.set_click_callback(back_callback)
self._back_button.set_enabled(lambda: self.enabled) # for nav stack
self._wifi_button = SmallerRoundedButton("wifi")
self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui)) self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui))
self._wifi_button.set_enabled(lambda: self.enabled)
self._continue_button = WidishRoundedButton("continue") self._show_time = 0.0
self._continue_button.set_enabled(False) self._pending_has_internet_scroll = False
self._pending_continue_grow_animation = False
self._pending_wifi_grow_animation = False
def on_waiting_click():
offset = (self._wifi_button.rect.x + self._wifi_button.rect.width / 2) - (self._rect.x + self._rect.width / 2)
self._scroller.scroll_to(offset, smooth=True, block_interaction=True)
# trigger grow when wifi button in view
self._pending_wifi_grow_animation = True
self._waiting_button = BigPillButton("waiting for\ninternet...", disabled_background=True)
self._waiting_button.set_click_callback(on_waiting_click)
self._continue_button = BigPillButton("install openpilot", green=True)
self._continue_button.set_click_callback(lambda: continue_callback(self._custom_software)) self._continue_button.set_click_callback(lambda: continue_callback(self._custom_software))
self._scroller.add_widgets([
self._connect_button,
self._wifi_button,
self._continue_button,
self._waiting_button,
])
gui_app.add_nav_stack_tick(self._nav_stack_tick) gui_app.add_nav_stack_tick(self._nav_stack_tick)
def show_event(self): def show_event(self):
super().show_event() super().show_event()
self._show_time = rl.get_time()
self._prev_has_internet = False self._prev_has_internet = False
self._network_monitor.reset() self._pending_has_internet_scroll = False
self._set_has_internet(False) self._pending_continue_grow_animation = False
self._pending_wifi_grow_animation = False
def _nav_stack_tick(self): def _nav_stack_tick(self):
self._wifi_manager.process_callbacks() self._wifi_manager.process_callbacks()
has_internet = self._network_monitor.network_connected.is_set() has_internet = self._network_monitor.network_connected.is_set()
if has_internet != self._prev_has_internet: if has_internet and not self._prev_has_internet:
self._set_has_internet(has_internet) self._pending_has_internet_scroll = True
if has_internet: self._prev_has_internet = has_internet
gui_app.pop_widgets_to(self)
self._prev_has_internet = has_internet
def _set_has_internet(self, has_internet: bool): if self._pending_has_internet_scroll:
if has_internet: # Scrolls over to continue button, then grows once in view
self._network_header.set_title("connected to internet") elapsed = rl.get_time() - self._show_time
self._network_header.set_icon(self._wifi_full_txt) if elapsed > 0.5:
self._continue_button.set_enabled(lambda: self.enabled) self._pending_has_internet_scroll = False
else:
self._network_header.set_title(self._waiting_text) def scroll_to_download():
self._network_header.set_icon(self._no_wifi_txt) self._scroller._layout()
self._continue_button.set_enabled(False) end_offset = -(self._scroller.content_size - self._rect.width)
remaining = self._scroller.scroll_panel.get_offset() - end_offset
self._scroller.scroll_to(remaining, smooth=True, block_interaction=True)
self._pending_continue_grow_animation = True
# Animate WifiUi down first before scroll
gui_app.pop_widgets_to(self, scroll_to_download)
def set_custom_software(self, custom_software: bool): def set_custom_software(self, custom_software: bool):
self._custom_software = custom_software self._custom_software = custom_software
self._continue_button.set_text("install openpilot" if not custom_software else "choose software")
self._continue_button.set_green(not custom_software)
def _render(self, _): def set_is_updater(self):
self._network_header.render(rl.Rectangle( self._continue_button.set_text("download\n& install")
self._rect.x + 16, self._continue_button.set_green(False)
self._rect.y + 16,
self._rect.width - 32,
self._network_header.rect.height,
))
self._back_button.render(rl.Rectangle( def _update_state(self):
self._rect.x + 8, super()._update_state()
self._rect.y + self._rect.height - self._back_button.rect.height,
self._back_button.rect.width,
self._back_button.rect.height,
))
self._wifi_button.render(rl.Rectangle( if self._pending_continue_grow_animation:
self._rect.x + 8 + self._back_button.rect.width + 10, btn_right = self._continue_button.rect.x + self._continue_button.rect.width
self._rect.y + self._rect.height - self._wifi_button.rect.height, visible_right = self._rect.x + self._rect.width
self._wifi_button.rect.width, if btn_right < visible_right + 50:
self._wifi_button.rect.height, self._pending_continue_grow_animation = False
)) self._continue_button.trigger_grow_animation()
self._continue_button.render(rl.Rectangle( if self._pending_wifi_grow_animation and abs(self._wifi_button.rect.x - ITEM_SPACING) < 50:
self._rect.x + self._rect.width - self._continue_button.rect.width - 8, self._pending_wifi_grow_animation = False
self._rect.y + self._rect.height - self._continue_button.rect.height, self._wifi_button.trigger_grow_animation()
self._continue_button.rect.width,
self._continue_button.rect.height, if self._network_monitor.network_connected.is_set():
)) self._continue_button.set_visible(True)
self._waiting_button.set_visible(False)
else:
self._continue_button.set_visible(False)
self._waiting_button.set_visible(True)
class NetworkSetupPage(NetworkSetupPageBase, NavScroller):
def __init__(self, network_monitor: NetworkConnectivityMonitor, continue_callback: Callable[[bool], None],
back_callback: Callable[[], None] | None):
super().__init__(network_monitor, continue_callback)
self.set_back_callback(back_callback)
class Setup(Widget): class Setup(Widget):
@@ -557,13 +478,13 @@ class Setup(Widget):
self._start_page.set_click_callback(getting_started_button_callback) self._start_page.set_click_callback(getting_started_button_callback)
self._start_page.set_enabled(lambda: self.enabled) # for nav stack self._start_page.set_enabled(lambda: self.enabled) # for nav stack
self._network_setup_page = NetworkSetupPage(self._network_monitor, self._network_setup_continue_button_callback, self._network_setup_page = NetworkSetupPage(self._network_monitor, self._network_setup_continue_callback, self._pop_to_software_selection)
self._pop_to_software_selection)
self._software_selection_page = SoftwareSelectionPage(self._use_openpilot, lambda: gui_app.push_widget(self._custom_software_warning_page)) self._software_selection_page = SoftwareSelectionPage(self._use_openpilot, lambda: gui_app.push_widget(self._custom_software_warning_page))
self._download_failed_page = FailedPage(HARDWARE.reboot, self._pop_to_software_selection) self._download_failed_page = FailedPage(HARDWARE.reboot, self._pop_to_software_selection)
self._custom_software_warning_page = CustomSoftwareWarningPage(self._software_selection_custom_software_continue, self._pop_to_software_selection) self._custom_software_warning_page = CustomSoftwareWarningPage(lambda: self._push_network_setup(True), self._pop_to_software_selection)
self._downloading_page = DownloadingPage() self._downloading_page = DownloadingPage()
@@ -602,17 +523,14 @@ class Setup(Widget):
time.sleep(0.1) time.sleep(0.1)
gui_app.request_close() gui_app.request_close()
else: else:
self._push_network_setup(custom_software=False) self._push_network_setup()
def _push_network_setup(self, custom_software: bool): def _push_network_setup(self, custom_software: bool = False):
# to fire the correct continue callback later
self._network_setup_page.set_custom_software(custom_software) self._network_setup_page.set_custom_software(custom_software)
gui_app.push_widget(self._network_setup_page) gui_app.pop_widgets_to(self._software_selection_page, lambda: gui_app.push_widget(self._network_setup_page))
def _software_selection_custom_software_continue(self): def _network_setup_continue_callback(self, custom_software: bool):
gui_app.pop_widgets_to(self._software_selection_page, instant=True) # don't reset sliders
self._push_network_setup(custom_software=True)
def _network_setup_continue_button_callback(self, custom_software):
if not custom_software: if not custom_software:
gui_app.pop_widgets_to(self._software_selection_page, instant=True) # don't reset sliders gui_app.pop_widgets_to(self._software_selection_page, instant=True) # don't reset sliders
self._download(OPENPILOT_URL) self._download(OPENPILOT_URL)
@@ -623,7 +541,7 @@ class Setup(Widget):
gui_app.pop_widgets_to(self._software_selection_page, instant=True) # don't reset sliders gui_app.pop_widgets_to(self._software_selection_page, instant=True) # don't reset sliders
self._download(url) self._download(url)
keyboard = BigInputDialog("custom software URL", confirm_callback=handle_keyboard_result) keyboard = BigInputDialog("custom software URL...", confirm_callback=handle_keyboard_result, auto_return_to_letters="./")
gui_app.push_widget(keyboard) gui_app.push_widget(keyboard)
def _download(self, url: str): def _download(self, url: str):
+28 -23
View File
@@ -5,13 +5,14 @@ import threading
import pyray as rl import pyray as rl
from enum import IntEnum from enum import IntEnum
from openpilot.system.hardware import HARDWARE from openpilot.common.realtime import config_realtime_process, set_core_affinity
from openpilot.system.hardware import HARDWARE, TICI
from openpilot.common.swaglog import cloudlog
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.wifi_manager import WifiManager
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.button import FullRoundedButton from openpilot.system.ui.widgets.button import FullRoundedButton
from openpilot.system.ui.mici_setup import NetworkSetupPage, FailedPage, NetworkConnectivityMonitor from openpilot.system.ui.mici_setup import NetworkSetupPageBase, FailedPageBase, NetworkConnectivityMonitor
class Screen(IntEnum): class Screen(IntEnum):
@@ -32,16 +33,15 @@ class Updater(Widget):
self.progress_text = "loading" self.progress_text = "loading"
self.process = None self.process = None
self.update_thread = None self.update_thread = None
self._wifi_manager = WifiManager()
self._wifi_manager.set_active(True)
self._network_setup_page = NetworkSetupPage(self._wifi_manager, self._network_setup_continue_callback,
self._network_setup_back_callback)
self._network_setup_page.set_enabled(lambda: self.enabled) # for nav stack
self._network_monitor = NetworkConnectivityMonitor() self._network_monitor = NetworkConnectivityMonitor()
self._network_monitor.start() self._network_monitor.start()
# TODO: network page is rendered inline, not pushed on nav stack, so auto-dismiss on internet connect doesn't work
self._network_setup_page = NetworkSetupPageBase(self._network_monitor, self._network_setup_continue_callback,
disable_connect_hint=True)
self._network_setup_page.set_is_updater()
self._network_setup_page.set_enabled(lambda: self.enabled) # for nav stack
# Buttons # Buttons
self._continue_button = FullRoundedButton("continue") self._continue_button = FullRoundedButton("continue")
self._continue_button.set_click_callback(lambda: self.set_current_screen(Screen.WIFI)) self._continue_button.set_click_callback(lambda: self.set_current_screen(Screen.WIFI))
@@ -52,8 +52,8 @@ class Updater(Widget):
text_color=rl.Color(255, 255, 255, int(255 * 0.9)), text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
font_weight=FontWeight.ROMAN) font_weight=FontWeight.ROMAN)
self._update_failed_page = FailedPage(HARDWARE.reboot, self._update_failed_retry_callback, self._update_failed_page = FailedPageBase(HARDWARE.reboot, self._update_failed_retry_callback,
title="update failed") title="update failed")
self._progress_title_label = UnifiedLabel("", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), self._progress_title_label = UnifiedLabel("", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
font_weight=FontWeight.DISPLAY, line_height=0.8) font_weight=FontWeight.DISPLAY, line_height=0.8)
@@ -61,10 +61,7 @@ class Updater(Widget):
font_weight=FontWeight.ROMAN, font_weight=FontWeight.ROMAN,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
def _network_setup_back_callback(self): def _network_setup_continue_callback(self, _):
self.set_current_screen(Screen.PROMPT)
def _network_setup_continue_callback(self):
self.install_update() self.install_update()
def _update_failed_retry_callback(self): def _update_failed_retry_callback(self):
@@ -99,9 +96,13 @@ class Updater(Widget):
def _run_update_process(self): def _run_update_process(self):
# TODO: just import it and run in a thread without a subprocess # TODO: just import it and run in a thread without a subprocess
cmd = [self.updater, "--swap", self.manifest] try:
self.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cmd = [self.updater, "--swap", self.manifest]
text=True, bufsize=1, universal_newlines=True) self.process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
text=True, bufsize=1, universal_newlines=True)
except Exception:
self.set_current_screen(Screen.FAILED)
return
if self.process.stdout is not None: if self.process.stdout is not None:
for line in self.process.stdout: for line in self.process.stdout:
@@ -160,14 +161,10 @@ class Updater(Widget):
rect.height, rect.height,
)) ))
def _update_state(self):
self._wifi_manager.process_callbacks()
def _render(self, rect: rl.Rectangle): def _render(self, rect: rl.Rectangle):
if self.current_screen == Screen.PROMPT: if self.current_screen == Screen.PROMPT:
self.render_prompt_screen(rect) self.render_prompt_screen(rect)
elif self.current_screen == Screen.WIFI: elif self.current_screen == Screen.WIFI:
self._network_setup_page.set_has_internet(self._network_monitor.network_connected.is_set())
self._network_setup_page.render(rect) self._network_setup_page.render(rect)
elif self.current_screen == Screen.PROGRESS: elif self.current_screen == Screen.PROGRESS:
self.render_progress_screen(rect) self.render_progress_screen(rect)
@@ -179,6 +176,14 @@ class Updater(Widget):
def main(): def main():
config_realtime_process(0, 51)
# attempt to affine. AGNOS will start setup with all cores, should only fail when manually launching with screen off
if TICI:
try:
set_core_affinity([5])
except OSError:
cloudlog.exception("Failed to set core affinity for updater process")
if len(sys.argv) < 3: if len(sys.argv) < 3:
print("Usage: updater.py <updater_path> <manifest_path>") print("Usage: updater.py <updater_path> <manifest_path>")
sys.exit(1) sys.exit(1)
+8 -3
View File
@@ -67,9 +67,14 @@ class Updater(Widget):
def _run_update_process(self): def _run_update_process(self):
# TODO: just import it and run in a thread without a subprocess # TODO: just import it and run in a thread without a subprocess
cmd = [self.updater, "--swap", self.manifest] try:
self.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cmd = [self.updater, "--swap", self.manifest]
text=True, bufsize=1, universal_newlines=True) self.process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
text=True, bufsize=1, universal_newlines=True)
except Exception:
self.progress_text = "Update failed"
self.show_reboot_button = True
return
if self.process.stdout is not None: if self.process.stdout is not None:
for line in self.process.stdout: for line in self.process.stdout:
+1
View File
@@ -228,6 +228,7 @@ class SmallCircleIconButton(Widget):
class SmallButton(Widget): class SmallButton(Widget):
def __init__(self, text: str): def __init__(self, text: str):
super().__init__() super().__init__()
self._click_delay = 0.075
self._opacity_filter = FirstOrderFilter(1.0, 0.1, 1 / gui_app.target_fps) self._opacity_filter = FirstOrderFilter(1.0, 0.1, 1 / gui_app.target_fps)
self._load_assets() self._load_assets()
+6 -1
View File
@@ -146,8 +146,9 @@ class CapsState(IntEnum):
class MiciKeyboard(Widget): class MiciKeyboard(Widget):
def __init__(self): def __init__(self, auto_return_to_letters: str = ""):
super().__init__() super().__init__()
self._auto_return_to_letters = auto_return_to_letters
lower_chars = [ lower_chars = [
"qwertyuiop", "qwertyuiop",
@@ -305,6 +306,10 @@ class MiciKeyboard(Widget):
if self._caps_state == CapsState.UPPER: if self._caps_state == CapsState.UPPER:
self._set_uppercase(False) self._set_uppercase(False)
# Switch back to letters after common URL delimiters
if self._closest_key[0].char in self._auto_return_to_letters and self._current_keys in (self._special_keys, self._super_special_keys):
self._set_uppercase(False)
# ensure minimum selected animation time # ensure minimum selected animation time
key_selected_dt = rl.get_time() - (self._selected_key_t or 0) key_selected_dt = rl.get_time() - (self._selected_key_t or 0)
cur_t = rl.get_time() cur_t = rl.get_time()
+4 -4
View File
@@ -63,7 +63,7 @@ class NavWidget(Widget, abc.ABC):
self._playing_dismiss_animation = False # released and animating away self._playing_dismiss_animation = False # released and animating away
self._y_pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1) self._y_pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1)
self._back_callback: Callable[[], None] | None = None # persistent callback for any back navigation self._back_callback: Callable[[], None] | None = None # persistent callback for user-initiated back navigation
self._dismiss_callback: Callable[[], None] | None = None # transient callback for programmatic dismiss self._dismiss_callback: Callable[[], None] | None = None # transient callback for programmatic dismiss
# TODO: move this state into NavBar # TODO: move this state into NavBar
@@ -150,12 +150,12 @@ class NavWidget(Widget, abc.ABC):
if new_y > self._rect.height + DISMISS_PUSH_OFFSET - 10: if new_y > self._rect.height + DISMISS_PUSH_OFFSET - 10:
gui_app.pop_widget() gui_app.pop_widget()
if self._back_callback is not None: # Only one callback should ever be fired
self._back_callback()
if self._dismiss_callback is not None: if self._dismiss_callback is not None:
self._dismiss_callback() self._dismiss_callback()
self._dismiss_callback = None self._dismiss_callback = None
elif self._back_callback is not None:
self._back_callback()
self._playing_dismiss_animation = False self._playing_dismiss_animation = False
self._drag_start_pos = None self._drag_start_pos = None
+2 -5
View File
@@ -13,7 +13,6 @@ if arch == "Darwin":
has_qt = False has_qt = False
else: else:
has_qt = shutil.which('qmake') is not None has_qt = shutil.which('qmake') is not None
if not has_qt: if not has_qt:
Return() Return()
@@ -21,7 +20,7 @@ SConscript(['#tools/replay/SConscript'])
Import('replay_lib') Import('replay_lib')
qt_env = env.Clone() qt_env = env.Clone()
qt_modules = ["Widgets", "Gui", "Core", "Network", "Concurrent", "DBus", "Xml"] qt_modules = ["Widgets", "Gui", "Core"]
qt_libs = [] qt_libs = []
if arch == "Darwin": if arch == "Darwin":
@@ -51,7 +50,7 @@ else:
qt_env['QT3DIR'] = qt_env['QTDIR'] qt_env['QT3DIR'] = qt_env['QTDIR']
qt_env.Tool('qt3') qt_env.Tool('qt3')
qt_env['CPPPATH'] += qt_dirs + ["#third_party/qrcode"] qt_env['CPPPATH'] += qt_dirs
qt_flags = [ qt_flags = [
"-D_REENTRANT", "-D_REENTRANT",
"-DQT_NO_DEBUG", "-DQT_NO_DEBUG",
@@ -69,10 +68,8 @@ base_libs = [common, messaging, cereal, visionipc, 'm', 'ssl', 'crypto', 'pthrea
if arch == "Darwin": if arch == "Darwin":
base_frameworks.append('QtCharts') base_frameworks.append('QtCharts')
base_frameworks.append('QtSerialBus')
else: else:
base_libs.append('Qt5Charts') base_libs.append('Qt5Charts')
base_libs.append('Qt5SerialBus')
qt_libs = base_libs qt_libs = base_libs
+12 -9
View File
@@ -111,7 +111,8 @@ void BinaryView::highlight(const cabana::Signal *sig) {
if (sig != hovered_sig) { if (sig != hovered_sig) {
for (int i = 0; i < model->items.size(); ++i) { for (int i = 0; i < model->items.size(); ++i) {
auto &item_sigs = model->items[i].sigs; auto &item_sigs = model->items[i].sigs;
if ((sig && item_sigs.contains(sig)) || (hovered_sig && item_sigs.contains(hovered_sig))) { auto has = [](const auto &v, auto p) { return std::find(v.begin(), v.end(), p) != v.end(); };
if ((sig && has(item_sigs, sig)) || (hovered_sig && has(item_sigs, hovered_sig))) {
auto index = model->index(i / model->columnCount(), i % model->columnCount()); auto index = model->index(i / model->columnCount(), i % model->columnCount());
emit model->dataChanged(index, index, {Qt::DisplayRole}); emit model->dataChanged(index, index, {Qt::DisplayRole});
} }
@@ -157,7 +158,7 @@ void BinaryView::mousePressEvent(QMouseEvent *event) {
void BinaryView::highlightPosition(const QPoint &pos) { void BinaryView::highlightPosition(const QPoint &pos) {
if (auto index = indexAt(viewport()->mapFromGlobal(pos)); index.isValid()) { if (auto index = indexAt(viewport()->mapFromGlobal(pos)); index.isValid()) {
auto item = (BinaryViewModel::Item *)index.internalPointer(); auto item = (BinaryViewModel::Item *)index.internalPointer();
const cabana::Signal *sig = item->sigs.isEmpty() ? nullptr : item->sigs.back(); const cabana::Signal *sig = item->sigs.empty() ? nullptr : item->sigs.back();
highlight(sig); highlight(sig);
} }
} }
@@ -208,12 +209,12 @@ void BinaryView::refresh() {
highlightPosition(QCursor::pos()); highlightPosition(QCursor::pos());
} }
QSet<const cabana::Signal *> BinaryView::getOverlappingSignals() const { std::set<const cabana::Signal *> BinaryView::getOverlappingSignals() const {
QSet<const cabana::Signal *> overlapping; std::set<const cabana::Signal *> overlapping;
for (const auto &item : model->items) { for (const auto &item : model->items) {
if (item.sigs.size() > 1) { if (item.sigs.size() > 1) {
for (auto s : item.sigs) { for (auto s : item.sigs) {
if (s->type == cabana::Signal::Type::Normal) overlapping += s; if (s->type == cabana::Signal::Type::Normal) overlapping.insert(s);
} }
} }
} }
@@ -258,7 +259,7 @@ void BinaryViewModel::refresh() {
int pos = sig->is_little_endian ? flipBitPos(sig->start_bit + j) : flipBitPos(sig->start_bit) + j; int pos = sig->is_little_endian ? flipBitPos(sig->start_bit + j) : flipBitPos(sig->start_bit) + j;
int idx = column_count * (pos / 8) + pos % 8; int idx = column_count * (pos / 8) + pos % 8;
if (idx >= items.size()) { if (idx >= items.size()) {
qWarning() << "signal " << sig->name << "out of bounds.start_bit:" << sig->start_bit << "size:" << sig->size; qWarning() << "signal " << sig->name.c_str() << "out of bounds.start_bit:" << sig->start_bit << "size:" << sig->size;
break; break;
} }
if (j == 0) sig->is_little_endian ? items[idx].is_lsb = true : items[idx].is_msb = true; if (j == 0) sig->is_little_endian ? items[idx].is_lsb = true : items[idx].is_msb = true;
@@ -404,7 +405,9 @@ bool BinaryItemDelegate::hasSignal(const QModelIndex &index, int dx, int dy, con
if (!index.isValid()) return false; if (!index.isValid()) return false;
auto model = (const BinaryViewModel*)(index.model()); auto model = (const BinaryViewModel*)(index.model());
int idx = (index.row() + dy) * model->columnCount() + index.column() + dx; int idx = (index.row() + dy) * model->columnCount() + index.column() + dx;
return (idx >=0 && idx < model->items.size()) ? model->items[idx].sigs.contains(sig) : false; if (idx < 0 || idx >= (int)model->items.size()) return false;
auto &s = model->items[idx].sigs;
return std::find(s.begin(), s.end(), sig) != s.end();
} }
void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
@@ -421,7 +424,7 @@ void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op
auto color = bin_view->resize_sig ? bin_view->resize_sig->color : option.palette.color(QPalette::Active, QPalette::Highlight); auto color = bin_view->resize_sig ? bin_view->resize_sig->color : option.palette.color(QPalette::Active, QPalette::Highlight);
painter->fillRect(option.rect, color); painter->fillRect(option.rect, color);
painter->setPen(option.palette.color(QPalette::BrightText)); painter->setPen(option.palette.color(QPalette::BrightText));
} else if (!bin_view->selectionModel()->hasSelection() || !item->sigs.contains(bin_view->resize_sig)) { // not resizing } else if (!bin_view->selectionModel()->hasSelection() || std::find(item->sigs.begin(), item->sigs.end(), bin_view->resize_sig) == item->sigs.end()) { // not resizing
if (item->sigs.size() > 0) { if (item->sigs.size() > 0) {
for (auto &s : item->sigs) { for (auto &s : item->sigs) {
if (s == bin_view->hovered_sig) { if (s == bin_view->hovered_sig) {
@@ -433,7 +436,7 @@ void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op
} else if (item->valid && item->bg_color.alpha() > 0) { } else if (item->valid && item->bg_color.alpha() > 0) {
painter->fillRect(option.rect, item->bg_color); painter->fillRect(option.rect, item->bg_color);
} }
auto color_role = item->sigs.contains(bin_view->hovered_sig) ? QPalette::BrightText : QPalette::Text; auto color_role = (std::find(item->sigs.begin(), item->sigs.end(), bin_view->hovered_sig) != item->sigs.end()) ? QPalette::BrightText : QPalette::Text;
painter->setPen(option.palette.color(bin_view->is_message_active ? QPalette::Normal : QPalette::Disabled, color_role)); painter->setPen(option.palette.color(bin_view->is_message_active ? QPalette::Normal : QPalette::Disabled, color_role));
} }
+3 -4
View File
@@ -1,10 +1,9 @@
#pragma once #pragma once
#include <set>
#include <tuple> #include <tuple>
#include <vector> #include <vector>
#include <QList>
#include <QSet>
#include <QStyledItemDelegate> #include <QStyledItemDelegate>
#include <QTableView> #include <QTableView>
@@ -51,7 +50,7 @@ public:
bool is_msb = false; bool is_msb = false;
bool is_lsb = false; bool is_lsb = false;
uint8_t val; uint8_t val;
QList<const cabana::Signal *> sigs; std::vector<const cabana::Signal *> sigs;
bool valid = false; bool valid = false;
}; };
std::vector<Item> items; std::vector<Item> items;
@@ -68,7 +67,7 @@ public:
BinaryView(QWidget *parent = nullptr); BinaryView(QWidget *parent = nullptr);
void setMessage(const MessageId &message_id); void setMessage(const MessageId &message_id);
void highlight(const cabana::Signal *sig); void highlight(const cabana::Signal *sig);
QSet<const cabana::Signal*> getOverlappingSignals() const; std::set<const cabana::Signal*> getOverlappingSignals() const;
void updateState() { model->updateState(); } void updateState() { model->updateState(); }
void paintEvent(QPaintEvent *event) override { void paintEvent(QPaintEvent *event) override {
is_message_active = can->isMessageActive(model->msg_id); is_message_active = can->isMessageActive(model->msg_id);
+3 -3
View File
@@ -46,13 +46,13 @@ int main(int argc, char *argv[]) {
stream = new DeviceStream(&app, cmd_parser.value("zmq")); stream = new DeviceStream(&app, cmd_parser.value("zmq"));
} else if (cmd_parser.isSet("panda") || cmd_parser.isSet("panda-serial")) { } else if (cmd_parser.isSet("panda") || cmd_parser.isSet("panda-serial")) {
try { try {
stream = new PandaStream(&app, {.serial = cmd_parser.value("panda-serial")}); stream = new PandaStream(&app, {.serial = cmd_parser.value("panda-serial").toStdString()});
} catch (std::exception &e) { } catch (std::exception &e) {
qWarning() << e.what(); qWarning() << e.what();
return 0; return 0;
} }
} else if (SocketCanStream::available() && cmd_parser.isSet("socketcan")) { } else if (SocketCanStream::available() && cmd_parser.isSet("socketcan")) {
stream = new SocketCanStream(&app, {.device = cmd_parser.value("socketcan")}); stream = new SocketCanStream(&app, {.device = cmd_parser.value("socketcan").toStdString()});
} else { } else {
uint32_t replay_flags = REPLAY_FLAG_NONE; uint32_t replay_flags = REPLAY_FLAG_NONE;
if (cmd_parser.isSet("ecam")) replay_flags |= REPLAY_FLAG_ECAM; if (cmd_parser.isSet("ecam")) replay_flags |= REPLAY_FLAG_ECAM;
@@ -70,7 +70,7 @@ int main(int argc, char *argv[]) {
if (!route.isEmpty()) { if (!route.isEmpty()) {
auto replay_stream = std::make_unique<ReplayStream>(&app); auto replay_stream = std::make_unique<ReplayStream>(&app);
bool auto_source = cmd_parser.isSet("auto"); bool auto_source = cmd_parser.isSet("auto");
if (!replay_stream->loadRoute(route, cmd_parser.value("data_dir"), replay_flags, auto_source)) { if (!replay_stream->loadRoute(route.toStdString(), cmd_parser.value("data_dir").toStdString(), replay_flags, auto_source)) {
return 0; return 0;
} }
stream = replay_stream.release(); stream = replay_stream.release();
+7 -7
View File
@@ -237,8 +237,8 @@ void ChartView::updateTitle() {
for (auto &s : sigs) { for (auto &s : sigs) {
auto decoration = s.series->isVisible() ? "none" : "line-through"; auto decoration = s.series->isVisible() ? "none" : "line-through";
s.series->setName(QString("<span style=\"text-decoration:%1; color:%2\"><b>%3</b> <font color=\"%4\">%5 %6</font></span>") s.series->setName(QString("<span style=\"text-decoration:%1; color:%2\"><b>%3</b> <font color=\"%4\">%5 %6</font></span>")
.arg(decoration, titleColorCss, s.sig->name, .arg(decoration, titleColorCss, QString::fromStdString(s.sig->name),
msgColorCss, msgName(s.msg_id), s.msg_id.toString())); msgColorCss, QString::fromStdString(msgName(s.msg_id)), QString::fromStdString(s.msg_id.toString())));
} }
split_chart_act->setEnabled(sigs.size() > 1); split_chart_act->setEnabled(sigs.size() > 1);
resetChartCache(); resetChartCache();
@@ -339,13 +339,13 @@ void ChartView::updateAxisY() {
double min = std::numeric_limits<double>::max(); double min = std::numeric_limits<double>::max();
double max = std::numeric_limits<double>::lowest(); double max = std::numeric_limits<double>::lowest();
QString unit = sigs[0].sig->unit; QString unit = QString::fromStdString(sigs[0].sig->unit);
for (auto &s : sigs) { for (auto &s : sigs) {
if (!s.series->isVisible()) continue; if (!s.series->isVisible()) continue;
// Only show unit when all signals have the same unit // Only show unit when all signals have the same unit
if (unit != s.sig->unit) { if (unit != QString::fromStdString(s.sig->unit)) {
unit.clear(); unit.clear();
} }
@@ -573,11 +573,11 @@ void ChartView::showTip(double sec) {
// use reverse iterator to find last item <= sec. // use reverse iterator to find last item <= sec.
auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), sec, [](auto &p, double v) { return p.x() > v; }); auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), sec, [](auto &p, double v) { return p.x() > v; });
if (it != s.vals.crend() && it->x() >= axis_x->min()) { if (it != s.vals.crend() && it->x() >= axis_x->min()) {
value = s.sig->formatValue(it->y(), false); value = QString::fromStdString(s.sig->formatValue(it->y(), false));
s.track_pt = *it; s.track_pt = *it;
x = std::max(x, chart()->mapToPosition(*it).x()); x = std::max(x, chart()->mapToPosition(*it).x());
} }
QString name = sigs.size() > 1 ? s.sig->name + ": " : ""; QString name = sigs.size() > 1 ? QString::fromStdString(s.sig->name) + ": " : "";
QString min = s.min == std::numeric_limits<double>::max() ? "--" : QString::number(s.min); QString min = s.min == std::numeric_limits<double>::max() ? "--" : QString::number(s.min);
QString max = s.max == std::numeric_limits<double>::lowest() ? "--" : QString::number(s.max); QString max = s.max == std::numeric_limits<double>::lowest() ? "--" : QString::number(s.max);
text_list << QString("<span style=\"color:%1;\">■ </span>%2<b>%3</b> (%4, %5)") text_list << QString("<span style=\"color:%1;\">■ </span>%2<b>%3</b> (%4, %5)")
@@ -766,7 +766,7 @@ void ChartView::drawSignalValue(QPainter *painter) {
for (auto &s : sigs) { for (auto &s : sigs) {
auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), cur_sec, auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), cur_sec,
[](auto &p, double x) { return p.x() > x + EPSILON; }); [](auto &p, double x) { return p.x() > x + EPSILON; });
QString value = (it != s.vals.crend() && it->x() >= axis_x->min()) ? s.sig->formatValue(it->y()) : "--"; QString value = (it != s.vals.crend() && it->x() >= axis_x->min()) ? QString::fromStdString(s.sig->formatValue(it->y())) : "--";
QRectF marker_rect = legend_markers[i++]->sceneBoundingRect(); QRectF marker_rect = legend_markers[i++]->sceneBoundingRect();
QRectF value_rect(marker_rect.bottomLeft() - QPoint(0, 1), marker_rect.size()); QRectF value_rect(marker_rect.bottomLeft() - QPoint(0, 1), marker_rect.size());
QString elided_val = painter->fontMetrics().elidedText(value, Qt::ElideRight, value_rect.width()); QString elided_val = painter->fontMetrics().elidedText(value, Qt::ElideRight, value_rect.width());
+24 -22
View File
@@ -1,13 +1,13 @@
#include "tools/cabana/chart/chartswidget.h" #include "tools/cabana/chart/chartswidget.h"
#include <algorithm> #include <algorithm>
#include <future>
#include <QApplication> #include <QApplication>
#include <QFutureSynchronizer>
#include <QMenu> #include <QMenu>
#include <QMimeData>
#include <QScrollBar> #include <QScrollBar>
#include <QToolBar> #include <QToolBar>
#include <QtConcurrent>
#include "tools/cabana/chart/chart.h" #include "tools/cabana/chart/chart.h"
@@ -166,15 +166,16 @@ void ChartsWidget::removeTab(int index) {
void ChartsWidget::updateTabBar() { void ChartsWidget::updateTabBar() {
for (int i = 0; i < tabbar->count(); ++i) { for (int i = 0; i < tabbar->count(); ++i) {
const auto &charts_in_tab = tab_charts[tabbar->tabData(i).toInt()]; const auto &charts_in_tab = tab_charts[tabbar->tabData(i).toInt()];
tabbar->setTabText(i, QString("Tab %1 (%2)").arg(i + 1).arg(charts_in_tab.count())); tabbar->setTabText(i, QString("Tab %1 (%2)").arg(i + 1).arg((int)charts_in_tab.size()));
} }
} }
void ChartsWidget::eventsMerged(const MessageEventsMap &new_events) { void ChartsWidget::eventsMerged(const MessageEventsMap &new_events) {
QFutureSynchronizer<void> future_synchronizer; std::vector<std::future<void>> futures;
for (auto c : charts) { for (auto c : charts) {
future_synchronizer.addFuture(QtConcurrent::run(c, &ChartView::updateSeries, nullptr, &new_events)); futures.push_back(std::async(std::launch::async, &ChartView::updateSeries, c, nullptr, &new_events));
} }
for (auto &f : futures) f.get();
} }
void ChartsWidget::timeRangeChanged(const std::optional<std::pair<double, double>> &time_range) { void ChartsWidget::timeRangeChanged(const std::optional<std::pair<double, double>> &time_range) {
@@ -203,7 +204,7 @@ void ChartsWidget::showValueTip(double sec) {
} }
void ChartsWidget::updateState() { void ChartsWidget::updateState() {
if (charts.isEmpty()) return; if (charts.empty()) return;
const auto &time_range = can->timeRange(); const auto &time_range = can->timeRange();
const double cur_sec = can->currentSec(); const double cur_sec = can->currentSec();
@@ -247,7 +248,7 @@ void ChartsWidget::updateToolBar() {
redo_zoom_action->setVisible(is_zoomed); redo_zoom_action->setVisible(is_zoomed);
reset_zoom_action->setVisible(is_zoomed); reset_zoom_action->setVisible(is_zoomed);
reset_zoom_btn->setText(is_zoomed ? tr("%1-%2").arg(can->timeRange()->first, 0, 'f', 2).arg(can->timeRange()->second, 0, 'f', 2) : ""); reset_zoom_btn->setText(is_zoomed ? tr("%1-%2").arg(can->timeRange()->first, 0, 'f', 2).arg(can->timeRange()->second, 0, 'f', 2) : "");
remove_all_btn->setEnabled(!charts.isEmpty()); remove_all_btn->setEnabled(!charts.empty());
} }
void ChartsWidget::settingChanged() { void ChartsWidget::settingChanged() {
@@ -281,9 +282,9 @@ ChartView *ChartsWidget::createChart(int pos) {
chart->setMinimumWidth(CHART_MIN_WIDTH); chart->setMinimumWidth(CHART_MIN_WIDTH);
chart->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); chart->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
QObject::connect(chart, &ChartView::axisYLabelWidthChanged, align_timer, qOverload<>(&QTimer::start)); QObject::connect(chart, &ChartView::axisYLabelWidthChanged, align_timer, qOverload<>(&QTimer::start));
pos = std::clamp(pos, 0, charts.size()); pos = std::clamp(pos, 0, (int)charts.size());
charts.insert(pos, chart); charts.insert(charts.begin() + pos, chart);
currentCharts().insert(pos, chart); currentCharts().insert(currentCharts().begin() + pos, chart);
updateLayout(true); updateLayout(true);
updateToolBar(); updateToolBar();
return chart; return chart;
@@ -302,7 +303,7 @@ void ChartsWidget::showChart(const MessageId &id, const cabana::Signal *sig, boo
void ChartsWidget::splitChart(ChartView *src_chart) { void ChartsWidget::splitChart(ChartView *src_chart) {
if (src_chart->sigs.size() > 1) { if (src_chart->sigs.size() > 1) {
int pos = charts.indexOf(src_chart) + 1; int pos = std::find(charts.begin(), charts.end(), src_chart) - charts.begin() + 1;
for (auto it = src_chart->sigs.begin() + 1; it != src_chart->sigs.end(); /**/) { for (auto it = src_chart->sigs.begin() + 1; it != src_chart->sigs.end(); /**/) {
auto c = createChart(pos); auto c = createChart(pos);
src_chart->chart()->removeSeries(it->series); src_chart->chart()->removeSeries(it->series);
@@ -327,7 +328,7 @@ QStringList ChartsWidget::serializeChartIds() const {
for (auto c : charts) { for (auto c : charts) {
QStringList ids; QStringList ids;
for (const auto& s : c->sigs) for (const auto& s : c->sigs)
ids += QString("%1|%2").arg(s.msg_id.toString(), s.sig->name); ids += QString("%1|%2").arg(QString::fromStdString(s.msg_id.toString()), QString::fromStdString(s.sig->name));
chart_ids += ids.join(','); chart_ids += ids.join(',');
} }
std::reverse(chart_ids.begin(), chart_ids.end()); std::reverse(chart_ids.begin(), chart_ids.end());
@@ -340,9 +341,9 @@ void ChartsWidget::restoreChartsFromIds(const QStringList& chart_ids) {
for (const auto& part : chart_id.split(',')) { for (const auto& part : chart_id.split(',')) {
const auto sig_parts = part.split('|'); const auto sig_parts = part.split('|');
if (sig_parts.size() != 2) continue; if (sig_parts.size() != 2) continue;
MessageId msg_id = MessageId::fromString(sig_parts[0]); MessageId msg_id = MessageId::fromString(sig_parts[0].toStdString());
if (auto* msg = dbc()->msg(msg_id)) if (auto* msg = dbc()->msg(msg_id))
if (auto* sig = msg->sig(sig_parts[1])) if (auto* sig = msg->sig(sig_parts[1].toStdString()))
showChart(msg_id, sig, true, index++ > 0); showChart(msg_id, sig, true, index++ > 0);
} }
} }
@@ -426,14 +427,14 @@ void ChartsWidget::doAutoScroll() {
} }
QSize ChartsWidget::minimumSizeHint() const { QSize ChartsWidget::minimumSizeHint() const {
return QSize(CHART_MIN_WIDTH * 1.5 * qApp->devicePixelRatio(), QWidget::minimumSizeHint().height()); return QSize(CHART_MIN_WIDTH * 1.5, QWidget::minimumSizeHint().height());
} }
void ChartsWidget::newChart() { void ChartsWidget::newChart() {
SignalSelector dlg(tr("New Chart"), this); SignalSelector dlg(tr("New Chart"), this);
if (dlg.exec() == QDialog::Accepted) { if (dlg.exec() == QDialog::Accepted) {
auto items = dlg.seletedItems(); auto items = dlg.seletedItems();
if (!items.isEmpty()) { if (!items.empty()) {
auto c = createChart(); auto c = createChart();
for (auto it : items) { for (auto it : items) {
c->addSignal(it->msg_id, it->sig); c->addSignal(it->msg_id, it->sig);
@@ -443,10 +444,10 @@ void ChartsWidget::newChart() {
} }
void ChartsWidget::removeChart(ChartView *chart) { void ChartsWidget::removeChart(ChartView *chart) {
charts.removeOne(chart); charts.erase(std::remove(charts.begin(), charts.end(), chart), charts.end());
chart->deleteLater(); chart->deleteLater();
for (auto &[_, list] : tab_charts) { for (auto &[_, list] : tab_charts) {
list.removeOne(chart); list.erase(std::remove(list.begin(), list.end(), chart), list.end());
} }
updateToolBar(); updateToolBar();
updateLayout(true); updateLayout(true);
@@ -460,7 +461,7 @@ void ChartsWidget::removeAll() {
} }
tab_charts.clear(); tab_charts.clear();
if (!charts.isEmpty()) { if (!charts.empty()) {
for (auto c : charts) { for (auto c : charts) {
delete c; delete c;
} }
@@ -560,10 +561,11 @@ void ChartsContainer::dropEvent(QDropEvent *event) {
auto chart = qobject_cast<ChartView *>(event->source()); auto chart = qobject_cast<ChartView *>(event->source());
if (w != chart) { if (w != chart) {
for (auto &[_, list] : charts_widget->tab_charts) { for (auto &[_, list] : charts_widget->tab_charts) {
list.removeOne(chart); list.erase(std::remove(list.begin(), list.end(), chart), list.end());
} }
int to = w ? charts_widget->currentCharts().indexOf(w) + 1 : 0; auto &cur = charts_widget->currentCharts();
charts_widget->currentCharts().insert(to, chart); int to = w ? std::find(cur.begin(), cur.end(), w) - cur.begin() + 1 : 0;
cur.insert(cur.begin() + to, chart);
charts_widget->updateLayout(true); charts_widget->updateLayout(true);
charts_widget->updateTabBar(); charts_widget->updateTabBar();
event->acceptProposedAction(); event->acceptProposedAction();
+3 -3
View File
@@ -81,7 +81,7 @@ private:
bool eventFilter(QObject *obj, QEvent *event) override; bool eventFilter(QObject *obj, QEvent *event) override;
void newTab(); void newTab();
void removeTab(int index); void removeTab(int index);
inline QList<ChartView *> &currentCharts() { return tab_charts[tabbar->tabData(tabbar->currentIndex()).toInt()]; } inline std::vector<ChartView *> &currentCharts() { return tab_charts[tabbar->tabData(tabbar->currentIndex()).toInt()]; }
ChartView *findChart(const MessageId &id, const cabana::Signal *sig); ChartView *findChart(const MessageId &id, const cabana::Signal *sig);
QLabel *title_label; QLabel *title_label;
@@ -100,8 +100,8 @@ private:
QUndoStack *zoom_undo_stack; QUndoStack *zoom_undo_stack;
ToolButton *remove_all_btn; ToolButton *remove_all_btn;
QList<ChartView *> charts; std::vector<ChartView *> charts;
std::unordered_map<int, QList<ChartView *>> tab_charts; std::unordered_map<int, std::vector<ChartView *>> tab_charts;
TabBar *tabbar; TabBar *tabbar;
ChartsContainer *charts_container; ChartsContainer *charts_container;
QScrollArea *charts_scroll; QScrollArea *charts_scroll;
+5 -5
View File
@@ -46,7 +46,7 @@ SignalSelector::SignalSelector(QString title, QWidget *parent) : QDialog(parent)
for (const auto &[id, _] : can->lastMessages()) { for (const auto &[id, _] : can->lastMessages()) {
if (auto m = dbc()->msg(id)) { if (auto m = dbc()->msg(id)) {
msgs_combo->addItem(QString("%1 (%2)").arg(m->name).arg(id.toString()), QVariant::fromValue(id)); msgs_combo->addItem(QString("%1 (%2)").arg(QString::fromStdString(m->name)).arg(QString::fromStdString(id.toString())), QVariant::fromValue(id));
} }
} }
msgs_combo->model()->sort(0); msgs_combo->model()->sort(0);
@@ -92,8 +92,8 @@ void SignalSelector::updateAvailableList(int index) {
} }
void SignalSelector::addItemToList(QListWidget *parent, const MessageId id, const cabana::Signal *sig, bool show_msg_name) { void SignalSelector::addItemToList(QListWidget *parent, const MessageId id, const cabana::Signal *sig, bool show_msg_name) {
QString text = QString("<span style=\"color:%0;\">■ </span> %1").arg(sig->color.name(), sig->name); QString text = QString("<span style=\"color:%0;\">■ </span> %1").arg(sig->color.name(), QString::fromStdString(sig->name));
if (show_msg_name) text += QString(" <font color=\"gray\">%0 %1</font>").arg(msgName(id), id.toString()); if (show_msg_name) text += QString(" <font color=\"gray\">%0 %1</font>").arg(QString::fromStdString(msgName(id)), QString::fromStdString(id.toString()));
QLabel *label = new QLabel(text); QLabel *label = new QLabel(text);
label->setContentsMargins(5, 0, 5, 0); label->setContentsMargins(5, 0, 5, 0);
@@ -102,8 +102,8 @@ void SignalSelector::addItemToList(QListWidget *parent, const MessageId id, cons
parent->setItemWidget(new_item, label); parent->setItemWidget(new_item, label);
} }
QList<SignalSelector::ListItem *> SignalSelector::seletedItems() { std::vector<SignalSelector::ListItem *> SignalSelector::seletedItems() {
QList<SignalSelector::ListItem *> ret; std::vector<SignalSelector::ListItem *> ret;
for (int i = 0; i < selected_list->count(); ++i) ret.push_back((ListItem *)selected_list->item(i)); for (int i = 0; i < selected_list->count(); ++i) ret.push_back((ListItem *)selected_list->item(i));
return ret; return ret;
} }
+1 -1
View File
@@ -15,7 +15,7 @@ public:
}; };
SignalSelector(QString title, QWidget *parent); SignalSelector(QString title, QWidget *parent);
QList<ListItem *> seletedItems(); std::vector<ListItem *> seletedItems();
inline void addSelected(const MessageId &id, const cabana::Signal *sig) { addItemToList(selected_list, id, sig, true); } inline void addSelected(const MessageId &id, const cabana::Signal *sig) { addItemToList(selected_list, id, sig, true); }
private: private:
+11 -11
View File
@@ -4,22 +4,22 @@
// EditMsgCommand // EditMsgCommand
EditMsgCommand::EditMsgCommand(const MessageId &id, const QString &name, int size, EditMsgCommand::EditMsgCommand(const MessageId &id, const std::string &name, int size,
const QString &node, const QString &comment, QUndoCommand *parent) const std::string &node, const std::string &comment, QUndoCommand *parent)
: id(id), new_name(name), new_size(size), new_node(node), new_comment(comment), QUndoCommand(parent) { : id(id), new_name(name), new_size(size), new_node(node), new_comment(comment), QUndoCommand(parent) {
if (auto msg = dbc()->msg(id)) { if (auto msg = dbc()->msg(id)) {
old_name = msg->name; old_name = msg->name;
old_size = msg->size; old_size = msg->size;
old_node = msg->transmitter; old_node = msg->transmitter;
old_comment = msg->comment; old_comment = msg->comment;
setText(QObject::tr("edit message %1:%2").arg(name).arg(id.address)); setText(QObject::tr("edit message %1:%2").arg(QString::fromStdString(name)).arg(id.address));
} else { } else {
setText(QObject::tr("new message %1:%2").arg(name).arg(id.address)); setText(QObject::tr("new message %1:%2").arg(QString::fromStdString(name)).arg(id.address));
} }
} }
void EditMsgCommand::undo() { void EditMsgCommand::undo() {
if (old_name.isEmpty()) if (old_name.empty())
dbc()->removeMsg(id); dbc()->removeMsg(id);
else else
dbc()->updateMsg(id, old_name, old_size, old_node, old_comment); dbc()->updateMsg(id, old_name, old_size, old_node, old_comment);
@@ -34,12 +34,12 @@ void EditMsgCommand::redo() {
RemoveMsgCommand::RemoveMsgCommand(const MessageId &id, QUndoCommand *parent) : id(id), QUndoCommand(parent) { RemoveMsgCommand::RemoveMsgCommand(const MessageId &id, QUndoCommand *parent) : id(id), QUndoCommand(parent) {
if (auto msg = dbc()->msg(id)) { if (auto msg = dbc()->msg(id)) {
message = *msg; message = *msg;
setText(QObject::tr("remove message %1:%2").arg(message.name).arg(id.address)); setText(QObject::tr("remove message %1:%2").arg(QString::fromStdString(message.name)).arg(id.address));
} }
} }
void RemoveMsgCommand::undo() { void RemoveMsgCommand::undo() {
if (!message.name.isEmpty()) { if (!message.name.empty()) {
dbc()->updateMsg(id, message.name, message.size, message.transmitter, message.comment); dbc()->updateMsg(id, message.name, message.size, message.transmitter, message.comment);
for (auto s : message.getSignals()) for (auto s : message.getSignals())
dbc()->addSignal(id, *s); dbc()->addSignal(id, *s);
@@ -47,7 +47,7 @@ void RemoveMsgCommand::undo() {
} }
void RemoveMsgCommand::redo() { void RemoveMsgCommand::redo() {
if (!message.name.isEmpty()) if (!message.name.empty())
dbc()->removeMsg(id); dbc()->removeMsg(id);
} }
@@ -55,7 +55,7 @@ void RemoveMsgCommand::redo() {
AddSigCommand::AddSigCommand(const MessageId &id, const cabana::Signal &sig, QUndoCommand *parent) AddSigCommand::AddSigCommand(const MessageId &id, const cabana::Signal &sig, QUndoCommand *parent)
: id(id), signal(sig), QUndoCommand(parent) { : id(id), signal(sig), QUndoCommand(parent) {
setText(QObject::tr("add signal %1 to %2:%3").arg(sig.name).arg(msgName(id)).arg(id.address)); setText(QObject::tr("add signal %1 to %2:%3").arg(QString::fromStdString(sig.name)).arg(QString::fromStdString(msgName(id))).arg(id.address));
} }
void AddSigCommand::undo() { void AddSigCommand::undo() {
@@ -85,7 +85,7 @@ RemoveSigCommand::RemoveSigCommand(const MessageId &id, const cabana::Signal *si
} }
} }
} }
setText(QObject::tr("remove signal %1 from %2:%3").arg(sig->name).arg(msgName(id)).arg(id.address)); setText(QObject::tr("remove signal %1 from %2:%3").arg(QString::fromStdString(sig->name)).arg(QString::fromStdString(msgName(id))).arg(id.address));
} }
void RemoveSigCommand::undo() { for (const auto &s : sigs) dbc()->addSignal(id, s); } void RemoveSigCommand::undo() { for (const auto &s : sigs) dbc()->addSignal(id, s); }
@@ -108,7 +108,7 @@ EditSignalCommand::EditSignalCommand(const MessageId &id, const cabana::Signal *
} }
} }
} }
setText(QObject::tr("edit signal %1 in %2:%3").arg(sig->name).arg(msgName(id)).arg(id.address)); setText(QObject::tr("edit signal %1 in %2:%3").arg(QString::fromStdString(sig->name)).arg(QString::fromStdString(msgName(id))).arg(id.address));
} }
void EditSignalCommand::undo() { for (const auto &s : sigs) dbc()->updateSignal(id, s.second.name, s.first); } void EditSignalCommand::undo() { for (const auto &s : sigs) dbc()->updateSignal(id, s.second.name, s.first); }
+7 -5
View File
@@ -1,6 +1,8 @@
#pragma once #pragma once
#include <string>
#include <utility> #include <utility>
#include <vector>
#include <QUndoCommand> #include <QUndoCommand>
#include <QUndoStack> #include <QUndoStack>
@@ -10,14 +12,14 @@
class EditMsgCommand : public QUndoCommand { class EditMsgCommand : public QUndoCommand {
public: public:
EditMsgCommand(const MessageId &id, const QString &name, int size, const QString &node, EditMsgCommand(const MessageId &id, const std::string &name, int size, const std::string &node,
const QString &comment, QUndoCommand *parent = nullptr); const std::string &comment, QUndoCommand *parent = nullptr);
void undo() override; void undo() override;
void redo() override; void redo() override;
private: private:
const MessageId id; const MessageId id;
QString old_name, new_name, old_comment, new_comment, old_node, new_node; std::string old_name, new_name, old_comment, new_comment, old_node, new_node;
int old_size = 0, new_size = 0; int old_size = 0, new_size = 0;
}; };
@@ -52,7 +54,7 @@ public:
private: private:
const MessageId id; const MessageId id;
QList<cabana::Signal> sigs; std::vector<cabana::Signal> sigs;
}; };
class EditSignalCommand : public QUndoCommand { class EditSignalCommand : public QUndoCommand {
@@ -63,7 +65,7 @@ public:
private: private:
const MessageId id; const MessageId id;
QList<std::pair<cabana::Signal, cabana::Signal>> sigs; // QList<{old_sig, new_sig}> std::vector<std::pair<cabana::Signal, cabana::Signal>> sigs; // {old_sig, new_sig}
}; };
namespace UndoStack { namespace UndoStack {
+14 -16
View File
@@ -4,10 +4,6 @@
#include "tools/cabana/utils/util.h" #include "tools/cabana/utils/util.h"
uint qHash(const MessageId &item) {
return qHash(item.source) ^ qHash(item.address);
}
// cabana::Msg // cabana::Msg
cabana::Msg::~Msg() { cabana::Msg::~Msg() {
@@ -22,7 +18,7 @@ cabana::Signal *cabana::Msg::addSignal(const cabana::Signal &sig) {
return s; return s;
} }
cabana::Signal *cabana::Msg::updateSignal(const QString &sig_name, const cabana::Signal &new_sig) { cabana::Signal *cabana::Msg::updateSignal(const std::string &sig_name, const cabana::Signal &new_sig) {
auto s = sig(sig_name); auto s = sig(sig_name);
if (s) { if (s) {
*s = new_sig; *s = new_sig;
@@ -31,7 +27,7 @@ cabana::Signal *cabana::Msg::updateSignal(const QString &sig_name, const cabana:
return s; return s;
} }
void cabana::Msg::removeSignal(const QString &sig_name) { void cabana::Msg::removeSignal(const std::string &sig_name) {
auto it = std::find_if(sigs.begin(), sigs.end(), [&](auto &s) { return s->name == sig_name; }); auto it = std::find_if(sigs.begin(), sigs.end(), [&](auto &s) { return s->name == sig_name; });
if (it != sigs.end()) { if (it != sigs.end()) {
delete *it; delete *it;
@@ -57,7 +53,7 @@ cabana::Msg &cabana::Msg::operator=(const cabana::Msg &other) {
return *this; return *this;
} }
cabana::Signal *cabana::Msg::sig(const QString &sig_name) const { cabana::Signal *cabana::Msg::sig(const std::string &sig_name) const {
auto it = std::find_if(sigs.begin(), sigs.end(), [&](auto &s) { return s->name == sig_name; }); auto it = std::find_if(sigs.begin(), sigs.end(), [&](auto &s) { return s->name == sig_name; });
return it != sigs.end() ? *it : nullptr; return it != sigs.end() ? *it : nullptr;
} }
@@ -69,17 +65,17 @@ int cabana::Msg::indexOf(const cabana::Signal *sig) const {
return -1; return -1;
} }
QString cabana::Msg::newSignalName() { std::string cabana::Msg::newSignalName() {
QString new_name; std::string new_name;
for (int i = 1; /**/; ++i) { for (int i = 1; /**/; ++i) {
new_name = QString("NEW_SIGNAL_%1").arg(i); new_name = "NEW_SIGNAL_" + std::to_string(i);
if (sig(new_name) == nullptr) break; if (sig(new_name) == nullptr) break;
} }
return new_name; return new_name;
} }
void cabana::Msg::update() { void cabana::Msg::update() {
if (transmitter.isEmpty()) { if (transmitter.empty()) {
transmitter = DEFAULT_NODE_NAME; transmitter = DEFAULT_NODE_NAME;
} }
mask.assign(size, 0x00); mask.assign(size, 0x00);
@@ -129,13 +125,13 @@ void cabana::Msg::update() {
void cabana::Signal::update() { void cabana::Signal::update() {
updateMsbLsb(*this); updateMsbLsb(*this);
if (receiver_name.isEmpty()) { if (receiver_name.empty()) {
receiver_name = DEFAULT_NODE_NAME; receiver_name = DEFAULT_NODE_NAME;
} }
float h = 19 * (float)lsb / 64.0; float h = 19 * (float)lsb / 64.0;
h = fmod(h, 1.0); h = fmod(h, 1.0);
size_t hash = qHash(name); size_t hash = std::hash<std::string>{}(name);
float s = 0.25 + 0.25 * (float)(hash & 0xff) / 255.0; float s = 0.25 + 0.25 * (float)(hash & 0xff) / 255.0;
float v = 0.75 + 0.25 * (float)((hash >> 8) & 0xff) / 255.0; float v = 0.75 + 0.25 * (float)((hash >> 8) & 0xff) / 255.0;
@@ -143,7 +139,7 @@ void cabana::Signal::update() {
precision = std::max(num_decimals(factor), num_decimals(offset)); precision = std::max(num_decimals(factor), num_decimals(offset));
} }
QString cabana::Signal::formatValue(double value, bool with_unit) const { std::string cabana::Signal::formatValue(double value, bool with_unit) const {
// Show enum string // Show enum string
int64_t raw_value = round((value - offset) / factor); int64_t raw_value = round((value - offset) / factor);
for (const auto &[val, desc] : val_desc) { for (const auto &[val, desc] : val_desc) {
@@ -152,8 +148,10 @@ QString cabana::Signal::formatValue(double value, bool with_unit) const {
} }
} }
QString val_str = QString::number(value, 'f', precision); char buf[64];
if (with_unit && !unit.isEmpty()) { snprintf(buf, sizeof(buf), "%.*f", precision, value);
std::string val_str(buf);
if (with_unit && !unit.empty()) {
val_str += " " + unit; val_str += " " + unit;
} }
return val_str; return val_str;
+37 -25
View File
@@ -1,29 +1,35 @@
#pragma once #pragma once
#include <cstdio>
#include <cstdlib>
#include <functional>
#include <limits> #include <limits>
#include <string>
#include <utility> #include <utility>
#include <vector> #include <vector>
#include <QColor> #include <QColor>
#include <QMetaType> #include <QMetaType>
#include <QString>
const QString UNTITLED = "untitled"; const std::string UNTITLED = "untitled";
const QString DEFAULT_NODE_NAME = "XXX"; const std::string DEFAULT_NODE_NAME = "XXX";
constexpr int CAN_MAX_DATA_BYTES = 64; constexpr int CAN_MAX_DATA_BYTES = 64;
struct MessageId { struct MessageId {
uint8_t source = 0; uint8_t source = 0;
uint32_t address = 0; uint32_t address = 0;
QString toString() const { std::string toString() const {
return QString("%1:%2").arg(source).arg(QString::number(address, 16).toUpper()); char buf[64];
snprintf(buf, sizeof(buf), "%u:%X", source, address);
return buf;
} }
inline static MessageId fromString(const QString &str) { inline static MessageId fromString(const std::string &str) {
auto parts = str.split(':'); auto pos = str.find(':');
if (parts.size() != 2) return {}; if (pos == std::string::npos) return {};
return MessageId{.source = uint8_t(parts[0].toUInt()), .address = parts[1].toUInt(nullptr, 16)}; return MessageId{.source = uint8_t(std::stoul(str.substr(0, pos))),
.address = uint32_t(std::stoul(str.substr(pos + 1), nullptr, 16))};
} }
bool operator==(const MessageId &other) const { bool operator==(const MessageId &other) const {
@@ -43,15 +49,17 @@ struct MessageId {
} }
}; };
uint qHash(const MessageId &item);
Q_DECLARE_METATYPE(MessageId); Q_DECLARE_METATYPE(MessageId);
template <> template <>
struct std::hash<MessageId> { struct std::hash<MessageId> {
std::size_t operator()(const MessageId &k) const noexcept { return qHash(k); } std::size_t operator()(const MessageId &k) const noexcept {
return std::hash<uint8_t>{}(k.source) ^ (std::hash<uint32_t>{}(k.address) << 1);
}
}; };
typedef std::vector<std::pair<double, QString>> ValueDescription; typedef std::vector<std::pair<double, std::string>> ValueDescription;
Q_DECLARE_METATYPE(ValueDescription);
namespace cabana { namespace cabana {
@@ -61,7 +69,7 @@ public:
Signal(const Signal &other) = default; Signal(const Signal &other) = default;
void update(); void update();
bool getValue(const uint8_t *data, size_t data_size, double *val) const; bool getValue(const uint8_t *data, size_t data_size, double *val) const;
QString formatValue(double value, bool with_unit = true) const; std::string formatValue(double value, bool with_unit = true) const;
bool operator==(const cabana::Signal &other) const; bool operator==(const cabana::Signal &other) const;
inline bool operator!=(const cabana::Signal &other) const { return !(*this == other); } inline bool operator!=(const cabana::Signal &other) const { return !(*this == other); }
@@ -72,16 +80,16 @@ public:
}; };
Type type = Type::Normal; Type type = Type::Normal;
QString name; std::string name;
int start_bit, msb, lsb, size; int start_bit, msb, lsb, size;
double factor = 1.0; double factor = 1.0;
double offset = 0; double offset = 0;
bool is_signed; bool is_signed;
bool is_little_endian; bool is_little_endian;
double min, max; double min, max;
QString unit; std::string unit;
QString comment; std::string comment;
QString receiver_name; std::string receiver_name;
ValueDescription val_desc; ValueDescription val_desc;
int precision = 0; int precision = 0;
QColor color; QColor color;
@@ -97,20 +105,20 @@ public:
Msg(const Msg &other) { *this = other; } Msg(const Msg &other) { *this = other; }
~Msg(); ~Msg();
cabana::Signal *addSignal(const cabana::Signal &sig); cabana::Signal *addSignal(const cabana::Signal &sig);
cabana::Signal *updateSignal(const QString &sig_name, const cabana::Signal &sig); cabana::Signal *updateSignal(const std::string &sig_name, const cabana::Signal &sig);
void removeSignal(const QString &sig_name); void removeSignal(const std::string &sig_name);
Msg &operator=(const Msg &other); Msg &operator=(const Msg &other);
int indexOf(const cabana::Signal *sig) const; int indexOf(const cabana::Signal *sig) const;
cabana::Signal *sig(const QString &sig_name) const; cabana::Signal *sig(const std::string &sig_name) const;
QString newSignalName(); std::string newSignalName();
void update(); void update();
inline const std::vector<cabana::Signal *> &getSignals() const { return sigs; } inline const std::vector<cabana::Signal *> &getSignals() const { return sigs; }
uint32_t address; uint32_t address;
QString name; std::string name;
uint32_t size; uint32_t size;
QString comment; std::string comment;
QString transmitter; std::string transmitter;
std::vector<cabana::Signal *> sigs; std::vector<cabana::Signal *> sigs;
std::vector<uint8_t> mask; std::vector<uint8_t> mask;
@@ -123,4 +131,8 @@ public:
double get_raw_value(const uint8_t *data, size_t data_size, const cabana::Signal &sig); double get_raw_value(const uint8_t *data, size_t data_size, const cabana::Signal &sig);
void updateMsbLsb(cabana::Signal &s); void updateMsbLsb(cabana::Signal &s);
inline int flipBitPos(int start_bit) { return 8 * (start_bit / 8) + 7 - start_bit % 8; } inline int flipBitPos(int start_bit) { return 8 * (start_bit / 8) + 7 - start_bit % 8; }
inline QString doubleToString(double value) { return QString::number(value, 'g', std::numeric_limits<double>::digits10); } inline std::string doubleToString(double value) {
char buf[64];
snprintf(buf, sizeof(buf), "%.*g", std::numeric_limits<double>::digits10, value);
return buf;
}
+59 -52
View File
@@ -3,11 +3,12 @@
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QRegularExpression> #include <QRegularExpression>
#include <QString>
DBCFile::DBCFile(const QString &dbc_file_name) { DBCFile::DBCFile(const std::string &dbc_file_name) {
QFile file(dbc_file_name); QFile file(QString::fromStdString(dbc_file_name));
if (file.open(QIODevice::ReadOnly)) { if (file.open(QIODevice::ReadOnly)) {
name_ = QFileInfo(dbc_file_name).baseName(); name_ = QFileInfo(QString::fromStdString(dbc_file_name)).baseName().toStdString();
filename = dbc_file_name; filename = dbc_file_name;
parse(file.readAll()); parse(file.readAll());
} else { } else {
@@ -15,34 +16,35 @@ DBCFile::DBCFile(const QString &dbc_file_name) {
} }
} }
DBCFile::DBCFile(const QString &name, const QString &content) : name_(name), filename("") { DBCFile::DBCFile(const std::string &name, const std::string &content) : name_(name), filename("") {
parse(content); parse(QString::fromStdString(content));
} }
bool DBCFile::save() { bool DBCFile::save() {
assert(!filename.isEmpty()); assert(!filename.empty());
return writeContents(filename); return writeContents(filename);
} }
bool DBCFile::saveAs(const QString &new_filename) { bool DBCFile::saveAs(const std::string &new_filename) {
filename = new_filename; filename = new_filename;
return save(); return save();
} }
bool DBCFile::writeContents(const QString &fn) { bool DBCFile::writeContents(const std::string &fn) {
QFile file(fn); QFile file(QString::fromStdString(fn));
if (file.open(QIODevice::WriteOnly)) { if (file.open(QIODevice::WriteOnly)) {
return file.write(generateDBC().toUtf8()) >= 0; std::string content = generateDBC();
return file.write(content.c_str(), content.size()) >= 0;
} }
return false; return false;
} }
void DBCFile::updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &node, const QString &comment) { void DBCFile::updateMsg(const MessageId &id, const std::string &name, uint32_t size, const std::string &node, const std::string &comment) {
auto &m = msgs[id.address]; auto &m = msgs[id.address];
m.address = id.address; m.address = id.address;
m.name = name; m.name = name;
m.size = size; m.size = size;
m.transmitter = node.isEmpty() ? DEFAULT_NODE_NAME : node; m.transmitter = node.empty() ? DEFAULT_NODE_NAME : node;
m.comment = comment; m.comment = comment;
} }
@@ -51,12 +53,12 @@ cabana::Msg *DBCFile::msg(uint32_t address) {
return it != msgs.end() ? &it->second : nullptr; return it != msgs.end() ? &it->second : nullptr;
} }
cabana::Msg *DBCFile::msg(const QString &name) { cabana::Msg *DBCFile::msg(const std::string &name) {
auto it = std::find_if(msgs.begin(), msgs.end(), [&name](auto &m) { return m.second.name == name; }); auto it = std::find_if(msgs.begin(), msgs.end(), [&name](auto &m) { return m.second.name == name; });
return it != msgs.end() ? &(it->second) : nullptr; return it != msgs.end() ? &(it->second) : nullptr;
} }
cabana::Signal *DBCFile::signal(uint32_t address, const QString &name) { cabana::Signal *DBCFile::signal(uint32_t address, const std::string &name) {
auto m = msg(address); auto m = msg(address);
return m ? (cabana::Signal *)m->sig(name) : nullptr; return m ? (cabana::Signal *)m->sig(name) : nullptr;
} }
@@ -93,13 +95,13 @@ void DBCFile::parse(const QString &content) {
seen = false; seen = false;
} }
} catch (std::exception &e) { } catch (std::exception &e) {
throw std::runtime_error(QString("[%1:%2]%3: %4").arg(filename).arg(line_num).arg(e.what()).arg(line).toStdString()); throw std::runtime_error(QString("[%1:%2]%3: %4").arg(QString::fromStdString(filename)).arg(line_num).arg(e.what()).arg(line).toStdString());
} }
if (seen) { if (seen) {
seen_first = true; seen_first = true;
} else if (!seen_first) { } else if (!seen_first) {
header += raw_line + "\n"; header += raw_line.toStdString() + "\n";
} }
} }
@@ -122,9 +124,9 @@ cabana::Msg *DBCFile::parseBO(const QString &line) {
// Create a new message object // Create a new message object
cabana::Msg *msg = &msgs[address]; cabana::Msg *msg = &msgs[address];
msg->address = address; msg->address = address;
msg->name = match.captured("name"); msg->name = match.captured("name").toStdString();
msg->size = match.captured("size").toULong(); msg->size = match.captured("size").toULong();
msg->transmitter = match.captured("transmitter").trimmed(); msg->transmitter = match.captured("transmitter").trimmed().toStdString();
return msg; return msg;
} }
@@ -141,7 +143,7 @@ void DBCFile::parseCM_BO(const QString &line, const QString &content, const QStr
throw std::runtime_error("Invalid message comment format"); throw std::runtime_error("Invalid message comment format");
if (auto m = (cabana::Msg *)msg(match.captured("address").toUInt())) if (auto m = (cabana::Msg *)msg(match.captured("address").toUInt()))
m->comment = match.captured("comment").trimmed().replace("\\\"", "\""); m->comment = match.captured("comment").trimmed().replace("\\\"", "\"").toStdString();
} }
void DBCFile::parseSG(const QString &line, cabana::Msg *current_msg, int &multiplexor_cnt) { void DBCFile::parseSG(const QString &line, cabana::Msg *current_msg, int &multiplexor_cnt) {
@@ -160,7 +162,7 @@ void DBCFile::parseSG(const QString &line, cabana::Msg *current_msg, int &multip
if (!match.hasMatch()) if (!match.hasMatch())
throw std::runtime_error("Invalid SG_ line format"); throw std::runtime_error("Invalid SG_ line format");
QString name = match.captured(1); std::string name = match.captured(1).toStdString();
if (current_msg->sig(name) != nullptr) if (current_msg->sig(name) != nullptr)
throw std::runtime_error("Duplicate signal name"); throw std::runtime_error("Duplicate signal name");
@@ -188,8 +190,8 @@ void DBCFile::parseSG(const QString &line, cabana::Msg *current_msg, int &multip
s.offset = match.captured(offset + 7).toDouble(); s.offset = match.captured(offset + 7).toDouble();
s.min = match.captured(8 + offset).toDouble(); s.min = match.captured(8 + offset).toDouble();
s.max = match.captured(9 + offset).toDouble(); s.max = match.captured(9 + offset).toDouble();
s.unit = match.captured(10 + offset); s.unit = match.captured(10 + offset).toStdString();
s.receiver_name = match.captured(11 + offset).trimmed(); s.receiver_name = match.captured(11 + offset).trimmed().toStdString();
current_msg->sigs.push_back(new cabana::Signal(s)); current_msg->sigs.push_back(new cabana::Signal(s));
} }
@@ -205,8 +207,8 @@ void DBCFile::parseCM_SG(const QString &line, const QString &content, const QStr
if (!match.hasMatch()) if (!match.hasMatch())
throw std::runtime_error("Invalid CM_ SG_ line format"); throw std::runtime_error("Invalid CM_ SG_ line format");
if (auto s = signal(match.captured(1).toUInt(), match.captured(2))) { if (auto s = signal(match.captured(1).toUInt(), match.captured(2).toStdString())) {
s->comment = match.captured(3).trimmed().replace("\\\"", "\""); s->comment = match.captured(3).trimmed().replace("\\\"", "\"").toStdString();
} }
} }
@@ -217,55 +219,60 @@ void DBCFile::parseVAL(const QString &line) {
if (!match.hasMatch()) if (!match.hasMatch())
throw std::runtime_error("invalid VAL_ line format"); throw std::runtime_error("invalid VAL_ line format");
if (auto s = signal(match.captured(1).toUInt(), match.captured(2))) { if (auto s = signal(match.captured(1).toUInt(), match.captured(2).toStdString())) {
QStringList desc_list = match.captured(3).trimmed().split('"'); QStringList desc_list = match.captured(3).trimmed().split('"');
for (int i = 0; i < desc_list.size(); i += 2) { for (int i = 0; i < desc_list.size(); i += 2) {
auto val = desc_list[i].trimmed(); auto val = desc_list[i].trimmed();
if (!val.isEmpty() && (i + 1) < desc_list.size()) { if (!val.isEmpty() && (i + 1) < desc_list.size()) {
auto desc = desc_list[i + 1].trimmed(); auto desc = desc_list[i + 1].trimmed();
s->val_desc.push_back({val.toDouble(), desc}); s->val_desc.push_back({val.toDouble(), desc.toStdString()});
} }
} }
} }
} }
QString DBCFile::generateDBC() { std::string DBCFile::generateDBC() {
QString dbc_string, comment, val_desc; std::string dbc_string, comment, val_desc;
for (const auto &[address, m] : msgs) { for (const auto &[address, m] : msgs) {
const QString transmitter = m.transmitter.isEmpty() ? DEFAULT_NODE_NAME : m.transmitter; const std::string &transmitter = m.transmitter.empty() ? DEFAULT_NODE_NAME : m.transmitter;
dbc_string += QString("BO_ %1 %2: %3 %4\n").arg(address).arg(m.name).arg(m.size).arg(transmitter); dbc_string += "BO_ " + std::to_string(address) + " " + m.name + ": " + std::to_string(m.size) + " " + transmitter + "\n";
if (!m.comment.isEmpty()) { if (!m.comment.empty()) {
comment += QString("CM_ BO_ %1 \"%2\";\n").arg(address).arg(QString(m.comment).replace("\"", "\\\"")); std::string escaped_comment = m.comment;
// Replace " with \"
for (size_t pos = 0; (pos = escaped_comment.find('"', pos)) != std::string::npos; pos += 2)
escaped_comment.replace(pos, 1, "\\\"");
comment += "CM_ BO_ " + std::to_string(address) + " \"" + escaped_comment + "\";\n";
} }
for (auto sig : m.getSignals()) { for (auto sig : m.getSignals()) {
QString multiplexer_indicator; std::string multiplexer_indicator;
if (sig->type == cabana::Signal::Type::Multiplexor) { if (sig->type == cabana::Signal::Type::Multiplexor) {
multiplexer_indicator = "M "; multiplexer_indicator = "M ";
} else if (sig->type == cabana::Signal::Type::Multiplexed) { } else if (sig->type == cabana::Signal::Type::Multiplexed) {
multiplexer_indicator = QString("m%1 ").arg(sig->multiplex_value); multiplexer_indicator = "m" + std::to_string(sig->multiplex_value) + " ";
} }
dbc_string += QString(" SG_ %1 %2: %3|%4@%5%6 (%7,%8) [%9|%10] \"%11\" %12\n") const std::string &recv = sig->receiver_name.empty() ? DEFAULT_NODE_NAME : sig->receiver_name;
.arg(sig->name) dbc_string += " SG_ " + sig->name + " " + multiplexer_indicator + ": " +
.arg(multiplexer_indicator) std::to_string(sig->start_bit) + "|" + std::to_string(sig->size) + "@" +
.arg(sig->start_bit) std::string(1, sig->is_little_endian ? '1' : '0') +
.arg(sig->size) std::string(1, sig->is_signed ? '-' : '+') +
.arg(sig->is_little_endian ? '1' : '0') " (" + doubleToString(sig->factor) + "," + doubleToString(sig->offset) + ")" +
.arg(sig->is_signed ? '-' : '+') " [" + doubleToString(sig->min) + "|" + doubleToString(sig->max) + "]" +
.arg(doubleToString(sig->factor)) " \"" + sig->unit + "\" " + recv + "\n";
.arg(doubleToString(sig->offset)) if (!sig->comment.empty()) {
.arg(doubleToString(sig->min)) std::string escaped_comment = sig->comment;
.arg(doubleToString(sig->max)) for (size_t pos = 0; (pos = escaped_comment.find('"', pos)) != std::string::npos; pos += 2)
.arg(sig->unit) escaped_comment.replace(pos, 1, "\\\"");
.arg(sig->receiver_name.isEmpty() ? DEFAULT_NODE_NAME : sig->receiver_name); comment += "CM_ SG_ " + std::to_string(address) + " " + sig->name + " \"" + escaped_comment + "\";\n";
if (!sig->comment.isEmpty()) {
comment += QString("CM_ SG_ %1 %2 \"%3\";\n").arg(address).arg(sig->name).arg(QString(sig->comment).replace("\"", "\\\""));
} }
if (!sig->val_desc.empty()) { if (!sig->val_desc.empty()) {
QStringList text; std::string text;
for (auto &[val, desc] : sig->val_desc) { for (auto &[val, desc] : sig->val_desc) {
text << QString("%1 \"%2\"").arg(val).arg(desc); if (!text.empty()) text += " ";
char val_buf[64];
snprintf(val_buf, sizeof(val_buf), "%g", val);
text += std::string(val_buf) + " \"" + desc + "\"";
} }
val_desc += QString("VAL_ %1 %2 %3;\n").arg(address).arg(sig->name).arg(text.join(" ")); val_desc += "VAL_ " + std::to_string(address) + " " + sig->name + " " + text + ";\n";
} }
} }
dbc_string += "\n"; dbc_string += "\n";
+14 -13
View File
@@ -1,34 +1,35 @@
#pragma once #pragma once
#include <map> #include <map>
#include <string>
#include <QTextStream> #include <QTextStream>
#include "tools/cabana/dbc/dbc.h" #include "tools/cabana/dbc/dbc.h"
class DBCFile { class DBCFile {
public: public:
DBCFile(const QString &dbc_file_name); DBCFile(const std::string &dbc_file_name);
DBCFile(const QString &name, const QString &content); DBCFile(const std::string &name, const std::string &content);
~DBCFile() {} ~DBCFile() {}
bool save(); bool save();
bool saveAs(const QString &new_filename); bool saveAs(const std::string &new_filename);
bool writeContents(const QString &fn); bool writeContents(const std::string &fn);
QString generateDBC(); std::string generateDBC();
void updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &node, const QString &comment); void updateMsg(const MessageId &id, const std::string &name, uint32_t size, const std::string &node, const std::string &comment);
inline void removeMsg(const MessageId &id) { msgs.erase(id.address); } inline void removeMsg(const MessageId &id) { msgs.erase(id.address); }
inline const std::map<uint32_t, cabana::Msg> &getMessages() const { return msgs; } inline const std::map<uint32_t, cabana::Msg> &getMessages() const { return msgs; }
cabana::Msg *msg(uint32_t address); cabana::Msg *msg(uint32_t address);
cabana::Msg *msg(const QString &name); cabana::Msg *msg(const std::string &name);
inline cabana::Msg *msg(const MessageId &id) { return msg(id.address); } inline cabana::Msg *msg(const MessageId &id) { return msg(id.address); }
cabana::Signal *signal(uint32_t address, const QString &name); cabana::Signal *signal(uint32_t address, const std::string &name);
inline QString name() const { return name_.isEmpty() ? "untitled" : name_; } inline std::string name() const { return name_.empty() ? "untitled" : name_; }
inline bool isEmpty() const { return msgs.empty() && name_.isEmpty(); } inline bool isEmpty() const { return msgs.empty() && name_.empty(); }
QString filename; std::string filename;
private: private:
void parse(const QString &content); void parse(const QString &content);
@@ -38,7 +39,7 @@ private:
void parseCM_SG(const QString &line, const QString &content, const QString &raw_line, const QTextStream &stream); void parseCM_SG(const QString &line, const QString &content, const QString &raw_line, const QTextStream &stream);
void parseVAL(const QString &line); void parseVAL(const QString &line);
QString header; std::string header;
std::map<uint32_t, cabana::Msg> msgs; std::map<uint32_t, cabana::Msg> msgs;
QString name_; std::string name_;
}; };
+23 -20
View File
@@ -1,10 +1,9 @@
#include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/dbc/dbcmanager.h"
#include <QSet>
#include <algorithm> #include <algorithm>
#include <numeric> #include <set>
bool DBCManager::open(const SourceSet &sources, const QString &dbc_file_name, QString *error) { bool DBCManager::open(const SourceSet &sources, const std::string &dbc_file_name, QString *error) {
try { try {
auto it = std::find_if(dbc_files.begin(), dbc_files.end(), auto it = std::find_if(dbc_files.begin(), dbc_files.end(),
[&](auto &f) { return f.second && f.second->filename == dbc_file_name; }); [&](auto &f) { return f.second && f.second->filename == dbc_file_name; });
@@ -21,7 +20,7 @@ bool DBCManager::open(const SourceSet &sources, const QString &dbc_file_name, QS
return true; return true;
} }
bool DBCManager::open(const SourceSet &sources, const QString &name, const QString &content, QString *error) { bool DBCManager::open(const SourceSet &sources, const std::string &name, const std::string &content, QString *error) {
try { try {
auto file = std::make_shared<DBCFile>(name, content); auto file = std::make_shared<DBCFile>(name, content);
for (auto s : sources) { for (auto s : sources) {
@@ -64,7 +63,7 @@ void DBCManager::addSignal(const MessageId &id, const cabana::Signal &sig) {
} }
} }
void DBCManager::updateSignal(const MessageId &id, const QString &sig_name, const cabana::Signal &sig) { void DBCManager::updateSignal(const MessageId &id, const std::string &sig_name, const cabana::Signal &sig) {
if (auto m = msg(id)) { if (auto m = msg(id)) {
if (auto s = m->updateSignal(sig_name, sig)) { if (auto s = m->updateSignal(sig_name, sig)) {
emit signalUpdated(s); emit signalUpdated(s);
@@ -73,7 +72,7 @@ void DBCManager::updateSignal(const MessageId &id, const QString &sig_name, cons
} }
} }
void DBCManager::removeSignal(const MessageId &id, const QString &sig_name) { void DBCManager::removeSignal(const MessageId &id, const std::string &sig_name) {
if (auto m = msg(id)) { if (auto m = msg(id)) {
if (auto s = m->sig(sig_name)) { if (auto s = m->sig(sig_name)) {
emit signalRemoved(s); emit signalRemoved(s);
@@ -83,7 +82,7 @@ void DBCManager::removeSignal(const MessageId &id, const QString &sig_name) {
} }
} }
void DBCManager::updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &node, const QString &comment) { void DBCManager::updateMsg(const MessageId &id, const std::string &name, uint32_t size, const std::string &node, const std::string &comment) {
auto dbc_file = findDBCFile(id); auto dbc_file = findDBCFile(id);
assert(dbc_file); // This should be impossible assert(dbc_file); // This should be impossible
dbc_file->updateMsg(id, name, size, node, comment); dbc_file->updateMsg(id, name, size, node, comment);
@@ -98,11 +97,13 @@ void DBCManager::removeMsg(const MessageId &id) {
emit maskUpdated(); emit maskUpdated();
} }
QString DBCManager::newMsgName(const MessageId &id) { std::string DBCManager::newMsgName(const MessageId &id) {
return QString("NEW_MSG_") + QString::number(id.address, 16).toUpper(); char buf[64];
snprintf(buf, sizeof(buf), "NEW_MSG_%X", id.address);
return buf;
} }
QString DBCManager::newSignalName(const MessageId &id) { std::string DBCManager::newSignalName(const MessageId &id) {
auto m = msg(id); auto m = msg(id);
return m ? m->newSignalName() : ""; return m ? m->newSignalName() : "";
} }
@@ -118,14 +119,14 @@ cabana::Msg *DBCManager::msg(const MessageId &id) {
return dbc_file ? dbc_file->msg(id) : nullptr; return dbc_file ? dbc_file->msg(id) : nullptr;
} }
cabana::Msg *DBCManager::msg(uint8_t source, const QString &name) { cabana::Msg *DBCManager::msg(uint8_t source, const std::string &name) {
auto dbc_file = findDBCFile(source); auto dbc_file = findDBCFile(source);
return dbc_file ? dbc_file->msg(name) : nullptr; return dbc_file ? dbc_file->msg(name) : nullptr;
} }
QStringList DBCManager::signalNames() { std::vector<std::string> DBCManager::signalNames() {
// Used for autocompletion // Used for autocompletion
QSet<QString> names; std::set<std::string> names;
for (auto &f : allDBCFiles()) { for (auto &f : allDBCFiles()) {
for (auto &[_, m] : f->getMessages()) { for (auto &[_, m] : f->getMessages()) {
for (auto sig : m.getSignals()) { for (auto sig : m.getSignals()) {
@@ -133,8 +134,8 @@ QStringList DBCManager::signalNames() {
} }
} }
} }
QStringList ret = names.values(); std::vector<std::string> ret(names.begin(), names.end());
ret.sort(); std::sort(ret.begin(), ret.end());
return ret; return ret;
} }
@@ -165,11 +166,13 @@ const SourceSet DBCManager::sources(const DBCFile *dbc_file) const {
return sources; return sources;
} }
QString toString(const SourceSet &ss) { std::string toString(const SourceSet &ss) {
return std::accumulate(ss.cbegin(), ss.cend(), QString(), [](QString str, int source) { std::string result;
if (!str.isEmpty()) str += ", "; for (int source : ss) {
return str + (source == -1 ? QStringLiteral("all") : QString::number(source)); if (!result.empty()) result += ", ";
}); result += (source == -1) ? "all" : std::to_string(source);
}
return result;
} }
DBCManager *dbc() { DBCManager *dbc() {
+13 -11
View File
@@ -4,6 +4,8 @@
#include <memory> #include <memory>
#include <map> #include <map>
#include <set> #include <set>
#include <string>
#include <vector>
#include "tools/cabana/dbc/dbcfile.h" #include "tools/cabana/dbc/dbcfile.h"
@@ -18,27 +20,27 @@ class DBCManager : public QObject {
public: public:
DBCManager(QObject *parent) : QObject(parent) {} DBCManager(QObject *parent) : QObject(parent) {}
~DBCManager() {} ~DBCManager() {}
bool open(const SourceSet &sources, const QString &dbc_file_name, QString *error = nullptr); bool open(const SourceSet &sources, const std::string &dbc_file_name, QString *error = nullptr);
bool open(const SourceSet &sources, const QString &name, const QString &content, QString *error = nullptr); bool open(const SourceSet &sources, const std::string &name, const std::string &content, QString *error = nullptr);
void close(const SourceSet &sources); void close(const SourceSet &sources);
void close(DBCFile *dbc_file); void close(DBCFile *dbc_file);
void closeAll(); void closeAll();
void addSignal(const MessageId &id, const cabana::Signal &sig); void addSignal(const MessageId &id, const cabana::Signal &sig);
void updateSignal(const MessageId &id, const QString &sig_name, const cabana::Signal &sig); void updateSignal(const MessageId &id, const std::string &sig_name, const cabana::Signal &sig);
void removeSignal(const MessageId &id, const QString &sig_name); void removeSignal(const MessageId &id, const std::string &sig_name);
void updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &node, const QString &comment); void updateMsg(const MessageId &id, const std::string &name, uint32_t size, const std::string &node, const std::string &comment);
void removeMsg(const MessageId &id); void removeMsg(const MessageId &id);
QString newMsgName(const MessageId &id); std::string newMsgName(const MessageId &id);
QString newSignalName(const MessageId &id); std::string newSignalName(const MessageId &id);
const std::map<uint32_t, cabana::Msg> &getMessages(uint8_t source); const std::map<uint32_t, cabana::Msg> &getMessages(uint8_t source);
cabana::Msg *msg(const MessageId &id); cabana::Msg *msg(const MessageId &id);
cabana::Msg* msg(uint8_t source, const QString &name); cabana::Msg* msg(uint8_t source, const std::string &name);
QStringList signalNames(); std::vector<std::string> signalNames();
inline int dbcCount() { return allDBCFiles().size(); } inline int dbcCount() { return allDBCFiles().size(); }
int nonEmptyDBCCount(); int nonEmptyDBCCount();
@@ -62,8 +64,8 @@ private:
DBCManager *dbc(); DBCManager *dbc();
QString toString(const SourceSet &ss); std::string toString(const SourceSet &ss);
inline QString msgName(const MessageId &id) { inline std::string msgName(const MessageId &id) {
auto msg = dbc()->msg(id); auto msg = dbc()->msg(id);
return msg ? msg->name : UNTITLED; return msg ? msg->name : UNTITLED;
} }
+16 -16
View File
@@ -124,9 +124,9 @@ int DetailWidget::findOrAddTab(const MessageId& message_id) {
if (tabbar->tabData(index).value<MessageId>() == message_id) break; if (tabbar->tabData(index).value<MessageId>() == message_id) break;
} }
if (index == -1) { if (index == -1) {
index = tabbar->addTab(message_id.toString()); index = tabbar->addTab(QString::fromStdString(message_id.toString()));
tabbar->setTabData(index, QVariant::fromValue(message_id)); tabbar->setTabData(index, QVariant::fromValue(message_id));
tabbar->setTabToolTip(index, msgName(message_id)); tabbar->setTabToolTip(index, QString::fromStdString(msgName(message_id)));
} }
return index; return index;
} }
@@ -151,21 +151,21 @@ std::pair<QString, QStringList> DetailWidget::serializeMessageIds() const {
QStringList msgs; QStringList msgs;
for (int i = 0; i < tabbar->count(); ++i) { for (int i = 0; i < tabbar->count(); ++i) {
MessageId id = tabbar->tabData(i).value<MessageId>(); MessageId id = tabbar->tabData(i).value<MessageId>();
msgs.append(id.toString()); msgs.append(QString::fromStdString(id.toString()));
} }
return std::make_pair(msg_id.toString(), msgs); return std::make_pair(QString::fromStdString(msg_id.toString()), msgs);
} }
void DetailWidget::restoreTabs(const QString active_msg_id, const QStringList& msg_ids) { void DetailWidget::restoreTabs(const QString active_msg_id, const QStringList& msg_ids) {
tabbar->blockSignals(true); tabbar->blockSignals(true);
for (const auto& str_id : msg_ids) { for (const auto& str_id : msg_ids) {
MessageId id = MessageId::fromString(str_id); MessageId id = MessageId::fromString(str_id.toStdString());
if (dbc()->msg(id) != nullptr) if (dbc()->msg(id) != nullptr)
findOrAddTab(id); findOrAddTab(id);
} }
tabbar->blockSignals(false); tabbar->blockSignals(false);
auto active_id = MessageId::fromString(active_msg_id); auto active_id = MessageId::fromString(active_msg_id.toStdString());
if (dbc()->msg(active_id) != nullptr) if (dbc()->msg(active_id) != nullptr)
setMessage(active_id); setMessage(active_id);
} }
@@ -180,10 +180,10 @@ void DetailWidget::refresh() {
warnings.push_back(tr("Message size (%1) is incorrect.").arg(msg->size)); warnings.push_back(tr("Message size (%1) is incorrect.").arg(msg->size));
} }
for (auto s : binary_view->getOverlappingSignals()) { for (auto s : binary_view->getOverlappingSignals()) {
warnings.push_back(tr("%1 has overlapping bits.").arg(s->name)); warnings.push_back(tr("%1 has overlapping bits.").arg(QString::fromStdString(s->name)));
} }
} }
QString msg_name = msg ? QString("%1 (%2)").arg(msg->name, msg->transmitter) : msgName(msg_id); QString msg_name = msg ? QString("%1 (%2)").arg(QString::fromStdString(msg->name), QString::fromStdString(msg->transmitter)) : QString::fromStdString(msgName(msg_id));
name_label->setText(msg_name); name_label->setText(msg_name);
name_label->setToolTip(msg_name); name_label->setToolTip(msg_name);
action_remove_msg->setEnabled(msg != nullptr); action_remove_msg->setEnabled(msg != nullptr);
@@ -208,10 +208,10 @@ void DetailWidget::updateState(const std::set<MessageId> *msgs) {
void DetailWidget::editMsg() { void DetailWidget::editMsg() {
auto msg = dbc()->msg(msg_id); auto msg = dbc()->msg(msg_id);
int size = msg ? msg->size : can->lastMessage(msg_id).dat.size(); int size = msg ? msg->size : can->lastMessage(msg_id).dat.size();
EditMessageDialog dlg(msg_id, msgName(msg_id), size, this); EditMessageDialog dlg(msg_id, QString::fromStdString(msgName(msg_id)), size, this);
if (dlg.exec()) { if (dlg.exec()) {
UndoStack::push(new EditMsgCommand(msg_id, dlg.name_edit->text().trimmed(), dlg.size_spin->value(), UndoStack::push(new EditMsgCommand(msg_id, dlg.name_edit->text().trimmed().toStdString(), dlg.size_spin->value(),
dlg.node->text().trimmed(), dlg.comment_edit->toPlainText().trimmed())); dlg.node->text().trimmed().toStdString(), dlg.comment_edit->toPlainText().trimmed().toStdString()));
} }
} }
@@ -223,7 +223,7 @@ void DetailWidget::removeMsg() {
EditMessageDialog::EditMessageDialog(const MessageId &msg_id, const QString &title, int size, QWidget *parent) EditMessageDialog::EditMessageDialog(const MessageId &msg_id, const QString &title, int size, QWidget *parent)
: original_name(title), msg_id(msg_id), QDialog(parent) { : original_name(title), msg_id(msg_id), QDialog(parent) {
setWindowTitle(tr("Edit message: %1").arg(msg_id.toString())); setWindowTitle(tr("Edit message: %1").arg(QString::fromStdString(msg_id.toString())));
QFormLayout *form_layout = new QFormLayout(this); QFormLayout *form_layout = new QFormLayout(this);
form_layout->addRow("", error_label = new QLabel); form_layout->addRow("", error_label = new QLabel);
@@ -241,8 +241,8 @@ EditMessageDialog::EditMessageDialog(const MessageId &msg_id, const QString &tit
form_layout->addRow(btn_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel)); form_layout->addRow(btn_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel));
if (auto msg = dbc()->msg(msg_id)) { if (auto msg = dbc()->msg(msg_id)) {
node->setText(msg->transmitter); node->setText(QString::fromStdString(msg->transmitter));
comment_edit->setText(msg->comment); comment_edit->setText(QString::fromStdString(msg->comment));
} }
validateName(name_edit->text()); validateName(name_edit->text());
setFixedWidth(parent->width() * 0.9); setFixedWidth(parent->width() * 0.9);
@@ -252,10 +252,10 @@ EditMessageDialog::EditMessageDialog(const MessageId &msg_id, const QString &tit
} }
void EditMessageDialog::validateName(const QString &text) { void EditMessageDialog::validateName(const QString &text) {
bool valid = text.compare(UNTITLED, Qt::CaseInsensitive) != 0; bool valid = text.compare(QString::fromStdString(UNTITLED), Qt::CaseInsensitive) != 0;
error_label->setVisible(false); error_label->setVisible(false);
if (!text.isEmpty() && valid && text != original_name) { if (!text.isEmpty() && valid && text != original_name) {
valid = dbc()->msg(msg_id.source, text) == nullptr; valid = dbc()->msg(msg_id.source, text.toStdString()) == nullptr;
if (!valid) { if (!valid) {
error_label->setText(tr("Name already exists")); error_label->setText(tr("Name already exists"));
error_label->setVisible(true); error_label->setVisible(true);
+6 -6
View File
@@ -14,7 +14,7 @@ QVariant HistoryLogModel::data(const QModelIndex &index, int role) const {
const int col = index.column(); const int col = index.column();
if (role == Qt::DisplayRole) { if (role == Qt::DisplayRole) {
if (col == 0) return QString::number(can->toSeconds(m.mono_time), 'f', 3); if (col == 0) return QString::number(can->toSeconds(m.mono_time), 'f', 3);
if (!isHexMode()) return sigs[col - 1]->formatValue(m.sig_values[col - 1], false); if (!isHexMode()) return QString::fromStdString(sigs[col - 1]->formatValue(m.sig_values[col - 1], false));
} else if (role == Qt::TextAlignmentRole) { } else if (role == Qt::TextAlignmentRole) {
return (uint32_t)(Qt::AlignRight | Qt::AlignVCenter); return (uint32_t)(Qt::AlignRight | Qt::AlignVCenter);
} }
@@ -49,8 +49,8 @@ QVariant HistoryLogModel::headerData(int section, Qt::Orientation orientation, i
if (section == 0) return "Time"; if (section == 0) return "Time";
if (isHexMode()) return "Data"; if (isHexMode()) return "Data";
QString name = sigs[section - 1]->name; QString name = QString::fromStdString(sigs[section - 1]->name);
QString unit = sigs[section - 1]->unit; QString unit = QString::fromStdString(sigs[section - 1]->unit);
return unit.isEmpty() ? name : QString("%1 (%2)").arg(name, unit); return unit.isEmpty() ? name : QString("%1 (%2)").arg(name, unit);
} else if (role == Qt::BackgroundRole && section > 0 && !isHexMode()) { } else if (role == Qt::BackgroundRole && section > 0 && !isHexMode()) {
// Alpha-blend the signal color with the background to ensure contrast // Alpha-blend the signal color with the background to ensure contrast
@@ -216,7 +216,7 @@ LogsWidget::LogsWidget(QWidget *parent) : QFrame(parent) {
void LogsWidget::modelReset() { void LogsWidget::modelReset() {
signals_cb->clear(); signals_cb->clear();
for (auto s : model->sigs) { for (auto s : model->sigs) {
signals_cb->addItem(s->name); signals_cb->addItem(QString::fromStdString(s->name));
} }
export_btn->setEnabled(false); export_btn->setEnabled(false);
value_edit->clear(); value_edit->clear();
@@ -238,8 +238,8 @@ void LogsWidget::filterChanged() {
} }
void LogsWidget::exportToCSV() { void LogsWidget::exportToCSV() {
QString dir = QString("%1/%2_%3.csv").arg(settings.last_dir).arg(can->routeName()).arg(msgName(model->msg_id)); QString dir = QString("%1/%2_%3.csv").arg(settings.last_dir).arg(QString::fromStdString(can->routeName())).arg(QString::fromStdString(msgName(model->msg_id)));
QString fn = QFileDialog::getSaveFileName(this, QString("Export %1 to CSV file").arg(msgName(model->msg_id)), QString fn = QFileDialog::getSaveFileName(this, QString("Export %1 to CSV file").arg(QString::fromStdString(msgName(model->msg_id))),
dir, tr("csv (*.csv)")); dir, tr("csv (*.csv)"));
if (!fn.isEmpty()) { if (!fn.isEmpty()) {
model->isHexMode() ? utils::exportToCSV(fn, model->msg_id) model->isHexMode() ? utils::exportToCSV(fn, model->msg_id)
+17 -17
View File
@@ -234,7 +234,7 @@ void MainWindow::DBCFileChanged() {
QStringList title; QStringList title;
for (auto f : dbc()->allDBCFiles()) { for (auto f : dbc()->allDBCFiles()) {
title.push_back(tr("(%1) %2").arg(toString(dbc()->sources(f)), f->name())); title.push_back(tr("(%1) %2").arg(QString::fromStdString(toString(dbc()->sources(f))), QString::fromStdString(f->name())));
} }
setWindowFilePath(title.join(" | ")); setWindowFilePath(title.join(" | "));
@@ -259,7 +259,7 @@ void MainWindow::closeStream() {
} }
void MainWindow::exportToCSV() { void MainWindow::exportToCSV() {
QString dir = QString("%1/%2.csv").arg(settings.last_dir).arg(can->routeName()); QString dir = QString("%1/%2.csv").arg(settings.last_dir).arg(QString::fromStdString(can->routeName()));
QString fn = QFileDialog::getSaveFileName(this, "Export stream to CSV file", dir, tr("csv (*.csv)")); QString fn = QFileDialog::getSaveFileName(this, "Export stream to CSV file", dir, tr("csv (*.csv)"));
if (!fn.isEmpty()) { if (!fn.isEmpty()) {
utils::exportToCSV(fn); utils::exportToCSV(fn);
@@ -268,7 +268,7 @@ void MainWindow::exportToCSV() {
void MainWindow::newFile(SourceSet s) { void MainWindow::newFile(SourceSet s) {
closeFile(s); closeFile(s);
dbc()->open(s, "", ""); dbc()->open(s, std::string(""), std::string(""));
} }
void MainWindow::openFile(SourceSet s) { void MainWindow::openFile(SourceSet s) {
@@ -284,7 +284,7 @@ void MainWindow::loadFile(const QString &fn, SourceSet s) {
closeFile(s); closeFile(s);
QString error; QString error;
if (dbc()->open(s, fn, &error)) { if (dbc()->open(s, fn.toStdString(), &error)) {
updateRecentFiles(fn); updateRecentFiles(fn);
statusBar()->showMessage(tr("DBC File %1 loaded").arg(fn), 2000); statusBar()->showMessage(tr("DBC File %1 loaded").arg(fn), 2000);
} else { } else {
@@ -304,7 +304,7 @@ void MainWindow::loadFromClipboard(SourceSet s, bool close_all) {
QString dbc_str = QGuiApplication::clipboard()->text(); QString dbc_str = QGuiApplication::clipboard()->text();
QString error; QString error;
bool ret = dbc()->open(s, "", dbc_str, &error); bool ret = dbc()->open(s, std::string(""), dbc_str.toStdString(), &error);
if (ret && dbc()->nonEmptyDBCCount() > 0) { if (ret && dbc()->nonEmptyDBCCount() > 0) {
QMessageBox::information(this, tr("Load From Clipboard"), tr("DBC Successfully Loaded!")); QMessageBox::information(this, tr("Load From Clipboard"), tr("DBC Successfully Loaded!"));
} else { } else {
@@ -333,7 +333,7 @@ void MainWindow::startStream(AbstractStream *stream, QString dbc_file) {
can->start(); can->start();
loadFile(dbc_file); loadFile(dbc_file);
statusBar()->showMessage(tr("Stream [%1] started").arg(can->routeName()), 2000); statusBar()->showMessage(tr("Stream [%1] started").arg(QString::fromStdString(can->routeName())), 2000);
bool has_stream = dynamic_cast<DummyStream *>(can) == nullptr; bool has_stream = dynamic_cast<DummyStream *>(can) == nullptr;
close_stream_act->setEnabled(has_stream); close_stream_act->setEnabled(has_stream);
@@ -341,7 +341,7 @@ void MainWindow::startStream(AbstractStream *stream, QString dbc_file) {
tools_menu->setEnabled(has_stream); tools_menu->setEnabled(has_stream);
createDockWidgets(); createDockWidgets();
video_dock->setWindowTitle(can->routeName()); video_dock->setWindowTitle(QString::fromStdString(can->routeName()));
if (can->liveStreaming() || video_splitter->sizes()[0] == 0) { if (can->liveStreaming() || video_splitter->sizes()[0] == 0) {
// display video at minimum size. // display video at minimum size.
video_splitter->setSizes({1, 1}); video_splitter->setSizes({1, 1});
@@ -368,9 +368,9 @@ void MainWindow::startStream(AbstractStream *stream, QString dbc_file) {
} }
void MainWindow::eventsMerged() { void MainWindow::eventsMerged() {
if (!can->liveStreaming() && std::exchange(car_fingerprint, can->carFingerprint()) != car_fingerprint) { if (!can->liveStreaming() && std::exchange(car_fingerprint, QString::fromStdString(can->carFingerprint())) != car_fingerprint) {
video_dock->setWindowTitle(tr("ROUTE: %1 FINGERPRINT: %2") video_dock->setWindowTitle(tr("ROUTE: %1 FINGERPRINT: %2")
.arg(can->routeName()) .arg(QString::fromStdString(can->routeName()))
.arg(car_fingerprint.isEmpty() ? tr("Unknown Car") : car_fingerprint)); .arg(car_fingerprint.isEmpty() ? tr("Unknown Car") : car_fingerprint));
// Don't overwrite already loaded DBC // Don't overwrite already loaded DBC
if (!dbc()->nonEmptyDBCCount() && fingerprint_to_dbc.object().contains(car_fingerprint)) { if (!dbc()->nonEmptyDBCCount() && fingerprint_to_dbc.object().contains(car_fingerprint)) {
@@ -416,7 +416,7 @@ void MainWindow::closeFile(DBCFile *dbc_file) {
void MainWindow::saveFile(DBCFile *dbc_file) { void MainWindow::saveFile(DBCFile *dbc_file) {
assert(dbc_file != nullptr); assert(dbc_file != nullptr);
if (!dbc_file->filename.isEmpty()) { if (!dbc_file->filename.empty()) {
dbc_file->save(); dbc_file->save();
UndoStack::instance()->setClean(); UndoStack::instance()->setClean();
statusBar()->showMessage(tr("File saved"), 2000); statusBar()->showMessage(tr("File saved"), 2000);
@@ -426,10 +426,10 @@ void MainWindow::saveFile(DBCFile *dbc_file) {
} }
void MainWindow::saveFileAs(DBCFile *dbc_file) { void MainWindow::saveFileAs(DBCFile *dbc_file) {
QString title = tr("Save File (bus: %1)").arg(toString(dbc()->sources(dbc_file))); QString title = tr("Save File (bus: %1)").arg(QString::fromStdString(toString(dbc()->sources(dbc_file))));
QString fn = QFileDialog::getSaveFileName(this, title, QDir::cleanPath(settings.last_dir + "/untitled.dbc"), tr("DBC (*.dbc)")); QString fn = QFileDialog::getSaveFileName(this, title, QDir::cleanPath(settings.last_dir + "/untitled.dbc"), tr("DBC (*.dbc)"));
if (!fn.isEmpty()) { if (!fn.isEmpty()) {
dbc_file->saveAs(fn); dbc_file->saveAs(fn.toStdString());
UndoStack::instance()->setClean(); UndoStack::instance()->setClean();
statusBar()->showMessage(tr("File saved as %1").arg(fn), 2000); statusBar()->showMessage(tr("File saved as %1").arg(fn), 2000);
updateRecentFiles(fn); updateRecentFiles(fn);
@@ -446,7 +446,7 @@ void MainWindow::saveToClipboard() {
void MainWindow::saveFileToClipboard(DBCFile *dbc_file) { void MainWindow::saveFileToClipboard(DBCFile *dbc_file) {
assert(dbc_file != nullptr); assert(dbc_file != nullptr);
QGuiApplication::clipboard()->setText(dbc_file->generateDBC()); QGuiApplication::clipboard()->setText(QString::fromStdString(dbc_file->generateDBC()));
QMessageBox::information(this, tr("Copy To Clipboard"), tr("DBC Successfully copied!")); QMessageBox::information(this, tr("Copy To Clipboard"), tr("DBC Successfully copied!"));
} }
@@ -467,14 +467,14 @@ void MainWindow::updateLoadSaveMenus() {
auto dbc_file = dbc()->findDBCFile(source); auto dbc_file = dbc()->findDBCFile(source);
if (dbc_file) { if (dbc_file) {
bus_menu->addSeparator(); bus_menu->addSeparator();
bus_menu->addAction(dbc_file->name() + " (" + toString(dbc()->sources(dbc_file)) + ")")->setEnabled(false); bus_menu->addAction(QString::fromStdString(dbc_file->name()) + " (" + QString::fromStdString(toString(dbc()->sources(dbc_file))) + ")")->setEnabled(false);
bus_menu->addAction(tr("Save..."), [=]() { saveFile(dbc_file); }); bus_menu->addAction(tr("Save..."), [=]() { saveFile(dbc_file); });
bus_menu->addAction(tr("Save As..."), [=]() { saveFileAs(dbc_file); }); bus_menu->addAction(tr("Save As..."), [=]() { saveFileAs(dbc_file); });
bus_menu->addAction(tr("Copy to Clipboard..."), [=]() { saveFileToClipboard(dbc_file); }); bus_menu->addAction(tr("Copy to Clipboard..."), [=]() { saveFileToClipboard(dbc_file); });
bus_menu->addAction(tr("Remove from this bus..."), [=]() { closeFile(ss); }); bus_menu->addAction(tr("Remove from this bus..."), [=]() { closeFile(ss); });
bus_menu->addAction(tr("Remove from all buses..."), [=]() { closeFile(dbc_file); }); bus_menu->addAction(tr("Remove from all buses..."), [=]() { closeFile(dbc_file); });
} }
bus_menu->setTitle(tr("Bus %1 (%2)").arg(source).arg(dbc_file ? dbc_file->name() : "No DBCs loaded")); bus_menu->setTitle(tr("Bus %1 (%2)").arg(source).arg(dbc_file ? QString::fromStdString(dbc_file->name()) : "No DBCs loaded"));
manage_dbcs_menu->addMenu(bus_menu); manage_dbcs_menu->addMenu(bus_menu);
} }
@@ -627,7 +627,7 @@ void MainWindow::saveSessionState() {
settings.active_charts.clear(); settings.active_charts.clear();
for (auto &f : dbc()->allDBCFiles()) for (auto &f : dbc()->allDBCFiles())
if (!f->isEmpty()) { settings.recent_dbc_file = f->filename; break; } if (!f->isEmpty()) { settings.recent_dbc_file = QString::fromStdString(f->filename); break; }
if (auto *detail = center_widget->getDetailWidget()) { if (auto *detail = center_widget->getDetailWidget()) {
auto [active_id, ids] = detail->serializeMessageIds(); auto [active_id, ids] = detail->serializeMessageIds();
@@ -643,7 +643,7 @@ void MainWindow::restoreSessionState() {
QString dbc_file; QString dbc_file;
for (auto& f : dbc()->allDBCFiles()) for (auto& f : dbc()->allDBCFiles())
if (!f->isEmpty()) { dbc_file = f->filename; break; } if (!f->isEmpty()) { dbc_file = QString::fromStdString(f->filename); break; }
if (dbc_file != settings.recent_dbc_file) return; if (dbc_file != settings.recent_dbc_file) return;
if (!settings.selected_msg_ids.isEmpty()) if (!settings.selected_msg_ids.isEmpty())
+4 -4
View File
@@ -205,7 +205,7 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const {
} else if (role == Qt::ToolTipRole && index.column() == Column::NAME) { } else if (role == Qt::ToolTipRole && index.column() == Column::NAME) {
auto msg = dbc()->msg(item.id); auto msg = dbc()->msg(item.id);
auto tooltip = item.name; auto tooltip = item.name;
if (msg && !msg->comment.isEmpty()) tooltip += "<br /><span style=\"color:gray;\">" + msg->comment + "</span>"; if (msg && !msg->comment.empty()) tooltip += "<br /><span style=\"color:gray;\">" + QString::fromStdString(msg->comment) + "</span>";
return tooltip; return tooltip;
} }
return {}; return {};
@@ -277,7 +277,7 @@ bool MessageListModel::match(const MessageListModel::Item &item) {
if (!match) { if (!match) {
const auto m = dbc()->msg(item.id); const auto m = dbc()->msg(item.id);
match = m && std::any_of(m->sigs.cbegin(), m->sigs.cend(), match = m && std::any_of(m->sigs.cbegin(), m->sigs.cend(),
[&txt](const auto &s) { return s->name.contains(txt, Qt::CaseInsensitive); }); [&txt](const auto &s) { return QString::fromStdString(s->name).contains(txt, Qt::CaseInsensitive); });
} }
break; break;
} }
@@ -323,8 +323,8 @@ bool MessageListModel::filterAndSort() {
if (show_inactive_messages || can->isMessageActive(id)) { if (show_inactive_messages || can->isMessageActive(id)) {
auto msg = dbc()->msg(id); auto msg = dbc()->msg(id);
Item item = {.id = id, Item item = {.id = id,
.name = msg ? msg->name : UNTITLED, .name = msg ? QString::fromStdString(msg->name) : QString::fromStdString(UNTITLED),
.node = msg ? msg->transmitter : QString()}; .node = msg ? QString::fromStdString(msg->transmitter) : QString()};
if (match(item)) if (match(item))
items.emplace_back(item); items.emplace_back(item);
} }
+37 -31
View File
@@ -1,6 +1,7 @@
#include "tools/cabana/signalview.h" #include "tools/cabana/signalview.h"
#include <algorithm> #include <algorithm>
#include <future>
#include <QCompleter> #include <QCompleter>
#include <QDialogButtonBox> #include <QDialogButtonBox>
@@ -11,7 +12,6 @@
#include <QPainterPath> #include <QPainterPath>
#include <QPushButton> #include <QPushButton>
#include <QScrollBar> #include <QScrollBar>
#include <QtConcurrent>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "tools/cabana/commands.h" #include "tools/cabana/commands.h"
@@ -34,8 +34,8 @@ SignalModel::SignalModel(QObject *parent) : root(new Item), QAbstractItemModel(p
} }
void SignalModel::insertItem(SignalModel::Item *root_item, int pos, const cabana::Signal *sig) { void SignalModel::insertItem(SignalModel::Item *root_item, int pos, const cabana::Signal *sig) {
Item *parent_item = new Item{.type = Item::Sig, .parent = root_item, .sig = sig, .title = sig->name}; Item *parent_item = new Item{.type = Item::Sig, .parent = root_item, .sig = sig, .title = QString::fromStdString(sig->name)};
root_item->children.insert(pos, parent_item); root_item->children.insert(root_item->children.begin() + pos, parent_item);
QString titles[]{"Name", "Size", "Receiver Nodes", "Little Endian", "Signed", "Offset", "Factor", "Type", QString titles[]{"Name", "Size", "Receiver Nodes", "Little Endian", "Signed", "Offset", "Factor", "Type",
"Multiplex Value", "Extra Info", "Unit", "Comment", "Minimum Value", "Maximum Value", "Value Table"}; "Multiplex Value", "Extra Info", "Unit", "Comment", "Minimum Value", "Maximum Value", "Value Table"};
for (int i = 0; i < std::size(titles); ++i) { for (int i = 0; i < std::size(titles); ++i) {
@@ -63,7 +63,7 @@ void SignalModel::refresh() {
root.reset(new SignalModel::Item); root.reset(new SignalModel::Item);
if (auto msg = dbc()->msg(msg_id)) { if (auto msg = dbc()->msg(msg_id)) {
for (auto s : msg->getSignals()) { for (auto s : msg->getSignals()) {
if (filter_str.isEmpty() || s->name.contains(filter_str, Qt::CaseInsensitive)) { if (filter_str.isEmpty() || QString::fromStdString(s->name).contains(filter_str, Qt::CaseInsensitive)) {
insertItem(root.get(), root->children.size(), s); insertItem(root.get(), root->children.size(), s);
} }
} }
@@ -124,25 +124,25 @@ QVariant SignalModel::data(const QModelIndex &index, int role) const {
const Item *item = getItem(index); const Item *item = getItem(index);
if (role == Qt::DisplayRole || role == Qt::EditRole) { if (role == Qt::DisplayRole || role == Qt::EditRole) {
if (index.column() == 0) { if (index.column() == 0) {
return item->type == Item::Sig ? item->sig->name : item->title; return item->type == Item::Sig ? QString::fromStdString(item->sig->name) : item->title;
} else { } else {
switch (item->type) { switch (item->type) {
case Item::Sig: return item->sig_val; case Item::Sig: return item->sig_val;
case Item::Name: return item->sig->name; case Item::Name: return QString::fromStdString(item->sig->name);
case Item::Size: return item->sig->size; case Item::Size: return item->sig->size;
case Item::Node: return item->sig->receiver_name; case Item::Node: return QString::fromStdString(item->sig->receiver_name);
case Item::SignalType: return signalTypeToString(item->sig->type); case Item::SignalType: return signalTypeToString(item->sig->type);
case Item::MultiplexValue: return item->sig->multiplex_value; case Item::MultiplexValue: return item->sig->multiplex_value;
case Item::Offset: return doubleToString(item->sig->offset); case Item::Offset: return QString::fromStdString(doubleToString(item->sig->offset));
case Item::Factor: return doubleToString(item->sig->factor); case Item::Factor: return QString::fromStdString(doubleToString(item->sig->factor));
case Item::Unit: return item->sig->unit; case Item::Unit: return QString::fromStdString(item->sig->unit);
case Item::Comment: return item->sig->comment; case Item::Comment: return QString::fromStdString(item->sig->comment);
case Item::Min: return doubleToString(item->sig->min); case Item::Min: return QString::fromStdString(doubleToString(item->sig->min));
case Item::Max: return doubleToString(item->sig->max); case Item::Max: return QString::fromStdString(doubleToString(item->sig->max));
case Item::Desc: { case Item::Desc: {
QStringList val_desc; QStringList val_desc;
for (auto &[val, desc] : item->sig->val_desc) { for (auto &[val, desc] : item->sig->val_desc) {
val_desc << QString("%1 \"%2\"").arg(val).arg(desc); val_desc << QString("%1 \"%2\"").arg(val).arg(QString::fromStdString(desc));
} }
return val_desc.join(" "); return val_desc.join(" ");
} }
@@ -165,17 +165,17 @@ bool SignalModel::setData(const QModelIndex &index, const QVariant &value, int r
Item *item = getItem(index); Item *item = getItem(index);
cabana::Signal s = *item->sig; cabana::Signal s = *item->sig;
switch (item->type) { switch (item->type) {
case Item::Name: s.name = value.toString(); break; case Item::Name: s.name = value.toString().toStdString(); break;
case Item::Size: s.size = value.toInt(); break; case Item::Size: s.size = value.toInt(); break;
case Item::Node: s.receiver_name = value.toString().trimmed(); break; case Item::Node: s.receiver_name = value.toString().trimmed().toStdString(); break;
case Item::SignalType: s.type = (cabana::Signal::Type)value.toInt(); break; case Item::SignalType: s.type = (cabana::Signal::Type)value.toInt(); break;
case Item::MultiplexValue: s.multiplex_value = value.toInt(); break; case Item::MultiplexValue: s.multiplex_value = value.toInt(); break;
case Item::Endian: s.is_little_endian = value.toBool(); break; case Item::Endian: s.is_little_endian = value.toBool(); break;
case Item::Signed: s.is_signed = value.toBool(); break; case Item::Signed: s.is_signed = value.toBool(); break;
case Item::Offset: s.offset = value.toDouble(); break; case Item::Offset: s.offset = value.toDouble(); break;
case Item::Factor: s.factor = value.toDouble(); break; case Item::Factor: s.factor = value.toDouble(); break;
case Item::Unit: s.unit = value.toString(); break; case Item::Unit: s.unit = value.toString().toStdString(); break;
case Item::Comment: s.comment = value.toString(); break; case Item::Comment: s.comment = value.toString().toStdString(); break;
case Item::Min: s.min = value.toDouble(); break; case Item::Min: s.min = value.toDouble(); break;
case Item::Max: s.max = value.toDouble(); break; case Item::Max: s.max = value.toDouble(); break;
case Item::Desc: s.val_desc = value.value<ValueDescription>(); break; case Item::Desc: s.val_desc = value.value<ValueDescription>(); break;
@@ -189,7 +189,7 @@ bool SignalModel::setData(const QModelIndex &index, const QVariant &value, int r
bool SignalModel::saveSignal(const cabana::Signal *origin_s, cabana::Signal &s) { bool SignalModel::saveSignal(const cabana::Signal *origin_s, cabana::Signal &s) {
auto msg = dbc()->msg(msg_id); auto msg = dbc()->msg(msg_id);
if (s.name != origin_s->name && msg->sig(s.name) != nullptr) { if (s.name != origin_s->name && msg->sig(s.name) != nullptr) {
QString text = tr("There is already a signal with the same name '%1'").arg(s.name); QString text = tr("There is already a signal with the same name '%1'").arg(QString::fromStdString(s.name));
QMessageBox::warning(nullptr, tr("Failed to save signal"), text); QMessageBox::warning(nullptr, tr("Failed to save signal"), text);
return false; return false;
} }
@@ -214,7 +214,7 @@ void SignalModel::handleSignalAdded(MessageId id, const cabana::Signal *sig) {
beginInsertRows({}, i, i); beginInsertRows({}, i, i);
insertItem(root.get(), i, sig); insertItem(root.get(), i, sig);
endInsertRows(); endInsertRows();
} else if (sig->name.contains(filter_str, Qt::CaseInsensitive)) { } else if (QString::fromStdString(sig->name).contains(filter_str, Qt::CaseInsensitive)) {
refresh(); refresh();
} }
} }
@@ -229,7 +229,9 @@ void SignalModel::handleSignalUpdated(const cabana::Signal *sig) {
int to = dbc()->msg(msg_id)->indexOf(sig); int to = dbc()->msg(msg_id)->indexOf(sig);
if (to != row) { if (to != row) {
beginMoveRows({}, row, row, {}, to > row ? to + 1 : to); beginMoveRows({}, row, row, {}, to > row ? to + 1 : to);
root->children.move(row, to); auto item = root->children[row];
root->children.erase(root->children.begin() + row);
root->children.insert(root->children.begin() + to, item);
endMoveRows(); endMoveRows();
} }
} }
@@ -239,7 +241,8 @@ void SignalModel::handleSignalUpdated(const cabana::Signal *sig) {
void SignalModel::handleSignalRemoved(const cabana::Signal *sig) { void SignalModel::handleSignalRemoved(const cabana::Signal *sig) {
if (int row = signalRow(sig); row != -1) { if (int row = signalRow(sig); row != -1) {
beginRemoveRows({}, row, row); beginRemoveRows({}, row, row);
delete root->children.takeAt(row); delete root->children[row];
root->children.erase(root->children.begin() + row);
endRemoveRows(); endRemoveRows();
} }
} }
@@ -373,7 +376,10 @@ QWidget *SignalItemDelegate::createEditor(QWidget *parent, const QStyleOptionVie
else e->setValidator(double_validator); else e->setValidator(double_validator);
if (item->type == SignalModel::Item::Name) { if (item->type == SignalModel::Item::Name) {
QCompleter *completer = new QCompleter(dbc()->signalNames(), e); auto names = dbc()->signalNames();
QStringList qnames;
for (const auto &n : names) qnames.push_back(QString::fromStdString(n));
QCompleter *completer = new QCompleter(qnames, e);
completer->setCaseSensitivity(Qt::CaseInsensitive); completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setFilterMode(Qt::MatchContains); completer->setFilterMode(Qt::MatchContains);
e->setCompleter(completer); e->setCompleter(completer);
@@ -395,7 +401,7 @@ QWidget *SignalItemDelegate::createEditor(QWidget *parent, const QStyleOptionVie
return c; return c;
} else if (item->type == SignalModel::Item::Desc) { } else if (item->type == SignalModel::Item::Desc) {
ValueDescriptionDlg dlg(item->sig->val_desc, parent); ValueDescriptionDlg dlg(item->sig->val_desc, parent);
dlg.setWindowTitle(item->sig->name); dlg.setWindowTitle(QString::fromStdString(item->sig->name));
if (dlg.exec()) { if (dlg.exec()) {
((QAbstractItemModel *)index.model())->setData(index, QVariant::fromValue(dlg.val_desc)); ((QAbstractItemModel *)index.model())->setData(index, QVariant::fromValue(dlg.val_desc));
} }
@@ -621,7 +627,7 @@ void SignalView::updateState(const std::set<MessageId> *msgs) {
for (auto item : model->root->children) { for (auto item : model->root->children) {
double value = 0; double value = 0;
if (item->sig->getValue(last_msg.dat.data(), last_msg.dat.size(), &value)) { if (item->sig->getValue(last_msg.dat.data(), last_msg.dat.size(), &value)) {
item->sig_val = item->sig->formatValue(value); item->sig_val = QString::fromStdString(item->sig->formatValue(value));
max_value_width = std::max(max_value_width, fontMetrics().horizontalAdvance(item->sig_val)); max_value_width = std::max(max_value_width, fontMetrics().horizontalAdvance(item->sig_val));
} }
} }
@@ -635,13 +641,13 @@ void SignalView::updateState(const std::set<MessageId> *msgs) {
delegate->button_size.height() - style()->pixelMetric(QStyle::PM_FocusFrameVMargin) * 2); delegate->button_size.height() - style()->pixelMetric(QStyle::PM_FocusFrameVMargin) * 2);
auto [first, last] = can->eventsInRange(model->msg_id, std::make_pair(last_msg.ts -settings.sparkline_range, last_msg.ts)); auto [first, last] = can->eventsInRange(model->msg_id, std::make_pair(last_msg.ts -settings.sparkline_range, last_msg.ts));
QFutureSynchronizer<void> synchronizer; std::vector<std::future<void>> futures;
for (int i = first_visible.row(); i <= last_visible.row(); ++i) { for (int i = first_visible.row(); i <= last_visible.row(); ++i) {
auto item = model->getItem(model->index(i, 1)); auto item = model->getItem(model->index(i, 1));
synchronizer.addFuture(QtConcurrent::run( futures.push_back(std::async(std::launch::async,
&item->sparkline, &Sparkline::update, item->sig, first, last, settings.sparkline_range, size)); &Sparkline::update, &item->sparkline, item->sig, first, last, settings.sparkline_range, size));
} }
synchronizer.waitForFinished(); for (auto &f : futures) f.get();
} }
for (int i = 0; i < model->rowCount(); ++i) { for (int i = 0; i < model->rowCount(); ++i) {
@@ -677,7 +683,7 @@ ValueDescriptionDlg::ValueDescriptionDlg(const ValueDescription &descriptions, Q
int row = 0; int row = 0;
for (auto &[val, desc] : descriptions) { for (auto &[val, desc] : descriptions) {
table->setItem(row, 0, new QTableWidgetItem(QString::number(val))); table->setItem(row, 0, new QTableWidgetItem(QString::number(val)));
table->setItem(row, 1, new QTableWidgetItem(desc)); table->setItem(row, 1, new QTableWidgetItem(QString::fromStdString(desc)));
++row; ++row;
} }
@@ -706,7 +712,7 @@ void ValueDescriptionDlg::save() {
QString val = table->item(i, 0)->text().trimmed(); QString val = table->item(i, 0)->text().trimmed();
QString desc = table->item(i, 1)->text().trimmed(); QString desc = table->item(i, 1)->text().trimmed();
if (!val.isEmpty() && !desc.isEmpty()) { if (!val.isEmpty() && !desc.isEmpty()) {
val_desc.push_back({val.toDouble(), desc}); val_desc.push_back({val.toDouble(), desc.toStdString()});
} }
} }
QDialog::accept(); QDialog::accept();
+6 -3
View File
@@ -20,12 +20,15 @@ class SignalModel : public QAbstractItemModel {
public: public:
struct Item { struct Item {
enum Type {Root, Sig, Name, Size, Node, Endian, Signed, Offset, Factor, SignalType, MultiplexValue, ExtraInfo, Unit, Comment, Min, Max, Desc }; enum Type {Root, Sig, Name, Size, Node, Endian, Signed, Offset, Factor, SignalType, MultiplexValue, ExtraInfo, Unit, Comment, Min, Max, Desc };
~Item() { qDeleteAll(children); } ~Item() { for (auto c : children) delete c; }
inline int row() { return parent->children.indexOf(this); } inline int row() {
auto it = std::find(parent->children.begin(), parent->children.end(), this);
return it != parent->children.end() ? std::distance(parent->children.begin(), it) : -1;
}
Type type = Type::Root; Type type = Type::Root;
Item *parent = nullptr; Item *parent = nullptr;
QList<Item *> children; std::vector<Item *> children;
const cabana::Signal *sig = nullptr; const cabana::Signal *sig = nullptr;
QString title; QString title;
+3 -3
View File
@@ -65,8 +65,8 @@ public:
virtual void start() = 0; virtual void start() = 0;
virtual bool liveStreaming() const { return true; } virtual bool liveStreaming() const { return true; }
virtual void seekTo(double ts) {} virtual void seekTo(double ts) {}
virtual QString routeName() const = 0; virtual std::string routeName() const = 0;
virtual QString carFingerprint() const { return ""; } virtual std::string carFingerprint() const { return ""; }
virtual QDateTime beginDateTime() const { return {}; } virtual QDateTime beginDateTime() const { return {}; }
virtual uint64_t beginMonoTime() const { return 0; } virtual uint64_t beginMonoTime() const { return 0; }
virtual double minSeconds() const { return 0; } virtual double minSeconds() const { return 0; }
@@ -149,7 +149,7 @@ class DummyStream : public AbstractStream {
Q_OBJECT Q_OBJECT
public: public:
DummyStream(QObject *parent) : AbstractStream(parent) {} DummyStream(QObject *parent) : AbstractStream(parent) {}
QString routeName() const override { return tr("No Stream"); } std::string routeName() const override { return "No Stream"; }
void start() override {} void start() override {}
}; };
+2 -2
View File
@@ -9,8 +9,8 @@ class DeviceStream : public LiveStream {
public: public:
DeviceStream(QObject *parent, QString address = {}); DeviceStream(QObject *parent, QString address = {});
~DeviceStream(); ~DeviceStream();
inline QString routeName() const override { inline std::string routeName() const override {
return QString("Live Streaming From %1").arg(zmq_address.isEmpty() ? "127.0.0.1" : zmq_address); return "Live Streaming From " + (zmq_address.isEmpty() ? std::string("127.0.0.1") : zmq_address.toStdString());
} }
protected: protected:
+4 -4
View File
@@ -16,8 +16,8 @@ PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(co
bool PandaStream::connect() { bool PandaStream::connect() {
try { try {
qDebug() << "Connecting to panda " << config.serial; qDebug() << "Connecting to panda " << config.serial.c_str();
panda.reset(new Panda(config.serial.toStdString())); panda.reset(new Panda(config.serial));
config.bus_config.resize(3); config.bus_config.resize(3);
qDebug() << "Connected"; qDebug() << "Connected";
} catch (const std::exception& e) { } catch (const std::exception& e) {
@@ -81,7 +81,7 @@ void PandaStream::streamThread() {
OpenPandaWidget::OpenPandaWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { OpenPandaWidget::OpenPandaWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) {
form_layout = new QFormLayout(this); form_layout = new QFormLayout(this);
if (can && dynamic_cast<PandaStream *>(can) != nullptr) { if (can && dynamic_cast<PandaStream *>(can) != nullptr) {
form_layout->addWidget(new QLabel(tr("Already connected to %1.").arg(can->routeName()))); form_layout->addWidget(new QLabel(tr("Already connected to %1.").arg(QString::fromStdString(can->routeName()))));
form_layout->addWidget(new QLabel("Close the current connection via [File menu -> Close Stream] before connecting to another Panda.")); form_layout->addWidget(new QLabel("Close the current connection via [File menu -> Close Stream] before connecting to another Panda."));
QTimer::singleShot(0, [this]() { emit enableOpenButton(false); }); QTimer::singleShot(0, [this]() { emit enableOpenButton(false); });
return; return;
@@ -129,7 +129,7 @@ void OpenPandaWidget::buildConfigForm() {
} }
if (has_panda) { if (has_panda) {
config.serial = serial; config.serial = serial.toStdString();
config.bus_config.resize(3); config.bus_config.resize(3);
for (int i = 0; i < config.bus_config.size(); i++) { for (int i = 0; i < config.bus_config.size(); i++) {
QHBoxLayout *bus_layout = new QHBoxLayout; QHBoxLayout *bus_layout = new QHBoxLayout;
+3 -3
View File
@@ -19,7 +19,7 @@ struct BusConfig {
}; };
struct PandaStreamConfig { struct PandaStreamConfig {
QString serial = ""; std::string serial = "";
std::vector<BusConfig> bus_config; std::vector<BusConfig> bus_config;
}; };
@@ -28,8 +28,8 @@ class PandaStream : public LiveStream {
public: public:
PandaStream(QObject *parent, PandaStreamConfig config_ = {}); PandaStream(QObject *parent, PandaStreamConfig config_ = {});
~PandaStream() { stop(); } ~PandaStream() { stop(); }
inline QString routeName() const override { inline std::string routeName() const override {
return QString("Panda: %1").arg(config.serial); return "Panda: " + config.serial;
} }
protected: protected:
+8 -8
View File
@@ -46,9 +46,9 @@ void ReplayStream::mergeSegments() {
} }
} }
bool ReplayStream::loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags, bool auto_source) { bool ReplayStream::loadRoute(const std::string &route, const std::string &data_dir, uint32_t replay_flags, bool auto_source) {
replay.reset(new Replay(route.toStdString(), {"can", "roadEncodeIdx", "driverEncodeIdx", "wideRoadEncodeIdx", "carParams"}, replay.reset(new Replay(route, {"can", "roadEncodeIdx", "driverEncodeIdx", "wideRoadEncodeIdx", "carParams"},
{}, nullptr, replay_flags, data_dir.toStdString(), auto_source)); {}, nullptr, replay_flags, data_dir, auto_source));
replay->setSegmentCacheLimit(settings.max_cached_minutes); replay->setSegmentCacheLimit(settings.max_cached_minutes);
replay->installEventFilter([this](const Event *event) { return eventFilter(event); }); replay->installEventFilter([this](const Event *event) { return eventFilter(event); });
@@ -72,17 +72,17 @@ bool ReplayStream::loadRoute(const QString &route, const QString &data_dir, uint
"This will grant access to routes from your comma account."; "This will grant access to routes from your comma account.";
} else { } else {
message = tr("Access Denied. You do not have permission to access route:\n\n%1\n\n" message = tr("Access Denied. You do not have permission to access route:\n\n%1\n\n"
"This is likely a private route.").arg(route); "This is likely a private route.").arg(QString::fromStdString(route));
} }
QMessageBox::warning(nullptr, tr("Access Denied"), message); QMessageBox::warning(nullptr, tr("Access Denied"), message);
} else if (replay->lastRouteError() == RouteLoadError::NetworkError) { } else if (replay->lastRouteError() == RouteLoadError::NetworkError) {
QMessageBox::warning(nullptr, tr("Network Error"), QMessageBox::warning(nullptr, tr("Network Error"),
tr("Unable to load the route:\n\n %1.\n\nPlease check your network connection and try again.").arg(route)); tr("Unable to load the route:\n\n %1.\n\nPlease check your network connection and try again.").arg(QString::fromStdString(route)));
} else if (replay->lastRouteError() == RouteLoadError::FileNotFound) { } else if (replay->lastRouteError() == RouteLoadError::FileNotFound) {
QMessageBox::warning(nullptr, tr("Route Not Found"), QMessageBox::warning(nullptr, tr("Route Not Found"),
tr("The specified route could not be found:\n\n %1.\n\nPlease check the route name and try again.").arg(route)); tr("The specified route could not be found:\n\n %1.\n\nPlease check the route name and try again.").arg(QString::fromStdString(route)));
} else { } else {
QMessageBox::warning(nullptr, tr("Route Load Failed"), tr("Failed to load route: '%1'").arg(route)); QMessageBox::warning(nullptr, tr("Route Load Failed"), tr("Failed to load route: '%1'").arg(QString::fromStdString(route)));
} }
} }
return success; return success;
@@ -168,7 +168,7 @@ AbstractStream *OpenReplayWidget::open() {
if (cameras[2]->isChecked()) flags |= REPLAY_FLAG_ECAM; if (cameras[2]->isChecked()) flags |= REPLAY_FLAG_ECAM;
if (flags == REPLAY_FLAG_NONE && !cameras[0]->isChecked()) flags = REPLAY_FLAG_NO_VIPC; if (flags == REPLAY_FLAG_NONE && !cameras[0]->isChecked()) flags = REPLAY_FLAG_NO_VIPC;
if (replay_stream->loadRoute(route, data_dir, flags)) { if (replay_stream->loadRoute(route.toStdString(), data_dir.toStdString(), flags)) {
return replay_stream.release(); return replay_stream.release();
} }
} }
+3 -3
View File
@@ -18,12 +18,12 @@ class ReplayStream : public AbstractStream {
public: public:
ReplayStream(QObject *parent); ReplayStream(QObject *parent);
void start() override { replay->start(); } void start() override { replay->start(); }
bool loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE, bool auto_source = false); bool loadRoute(const std::string &route, const std::string &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE, bool auto_source = false);
bool eventFilter(const Event *event); bool eventFilter(const Event *event);
void seekTo(double ts) override { replay->seekTo(std::max(double(0), ts), false); } void seekTo(double ts) override { replay->seekTo(std::max(double(0), ts), false); }
bool liveStreaming() const override { return false; } bool liveStreaming() const override { return false; }
inline QString routeName() const override { return QString::fromStdString(replay->route().name()); } inline std::string routeName() const override { return replay->route().name(); }
inline QString carFingerprint() const override { return replay->carFingerprint().c_str(); } inline std::string carFingerprint() const override { return replay->carFingerprint(); }
double minSeconds() const override { return replay->minSeconds(); } double minSeconds() const override { return replay->minSeconds(); }
double maxSeconds() const { return replay->maxSeconds(); } double maxSeconds() const { return replay->maxSeconds(); }
inline QDateTime beginDateTime() const { return QDateTime::fromSecsSinceEpoch(replay->routeDateTime()); } inline QDateTime beginDateTime() const { return QDateTime::fromSecsSinceEpoch(replay->routeDateTime()); }
+9 -11
View File
@@ -11,7 +11,7 @@
#include <QMessageBox> #include <QMessageBox>
#include <QPainter> #include <QPainter>
#include <QPointer> #include <QPointer>
#include <QtConcurrent> #include <thread>
#include "tools/replay/py_downloader.h" #include "tools/replay/py_downloader.h"
@@ -72,13 +72,12 @@ RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) {
// Fetch devices // Fetch devices
QPointer<RoutesDialog> self = this; QPointer<RoutesDialog> self = this;
QtConcurrent::run([self]() { std::thread([self]() {
std::string result = PyDownloader::getDevices(); std::string result = PyDownloader::getDevices();
auto [success, error_code] = checkApiResponse(result); QMetaObject::invokeMethod(qApp, [self, r = QString::fromStdString(result), response = checkApiResponse(result)]() {
QMetaObject::invokeMethod(qApp, [self, r = QString::fromStdString(result), success, error_code]() { if (self) self->parseDeviceList(r, response.first, response.second);
if (self) self->parseDeviceList(r, success, error_code);
}, Qt::QueuedConnection); }, Qt::QueuedConnection);
}); }).detach();
} }
void RoutesDialog::parseDeviceList(const QString &json, bool success, int error_code) { void RoutesDialog::parseDeviceList(const QString &json, bool success, int error_code) {
@@ -114,14 +113,13 @@ void RoutesDialog::fetchRoutes() {
int request_id = ++fetch_id_; int request_id = ++fetch_id_;
QPointer<RoutesDialog> self = this; QPointer<RoutesDialog> self = this;
QtConcurrent::run([self, did, start_ms, end_ms, preserved, request_id]() { std::thread([self, did, start_ms, end_ms, preserved, request_id]() {
std::string result = PyDownloader::getDeviceRoutes(did, start_ms, end_ms, preserved); std::string result = PyDownloader::getDeviceRoutes(did, start_ms, end_ms, preserved);
if (!self || self->fetch_id_ != request_id) return; if (!self || self->fetch_id_ != request_id) return;
auto [success, error_code] = checkApiResponse(result); QMetaObject::invokeMethod(qApp, [self, r = QString::fromStdString(result), response = checkApiResponse(result), request_id]() {
QMetaObject::invokeMethod(qApp, [self, r = QString::fromStdString(result), success, error_code, request_id]() { if (self && self->fetch_id_ == request_id) self->parseRouteList(r, response.first, response.second);
if (self && self->fetch_id_ == request_id) self->parseRouteList(r, success, error_code);
}, Qt::QueuedConnection); }, Qt::QueuedConnection);
}); }).detach();
} }
void RoutesDialog::parseRouteList(const QString &json, bool success, int error_code) { void RoutesDialog::parseRouteList(const QString &json, bool success, int error_code) {
+70 -32
View File
@@ -1,6 +1,14 @@
#include "tools/cabana/streams/socketcanstream.h" #include "tools/cabana/streams/socketcanstream.h"
#include <linux/can.h>
#include <linux/can/raw.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <unistd.h>
#include <QDebug> #include <QDebug>
#include <QDir>
#include <QFormLayout> #include <QFormLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QMessageBox> #include <QMessageBox>
@@ -9,59 +17,82 @@
SocketCanStream::SocketCanStream(QObject *parent, SocketCanStreamConfig config_) : config(config_), LiveStream(parent) { SocketCanStream::SocketCanStream(QObject *parent, SocketCanStreamConfig config_) : config(config_), LiveStream(parent) {
if (!available()) { if (!available()) {
throw std::runtime_error("SocketCAN plugin not available"); throw std::runtime_error("SocketCAN not available");
} }
qDebug() << "Connecting to SocketCAN device" << config.device; qDebug() << "Connecting to SocketCAN device" << config.device.c_str();
if (!connect()) { if (!connect()) {
throw std::runtime_error("Failed to connect to SocketCAN device"); throw std::runtime_error("Failed to connect to SocketCAN device");
} }
} }
SocketCanStream::~SocketCanStream() {
stop();
if (sock_fd >= 0) {
::close(sock_fd);
sock_fd = -1;
}
}
bool SocketCanStream::available() { bool SocketCanStream::available() {
return QCanBus::instance()->plugins().contains("socketcan"); int fd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if (fd < 0) return false;
::close(fd);
return true;
} }
bool SocketCanStream::connect() { bool SocketCanStream::connect() {
// Connecting might generate some warnings about missing socketcan/libsocketcan libraries sock_fd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
// These are expected and can be ignored, we don't need the advanced features of libsocketcan if (sock_fd < 0) {
QString errorString; qDebug() << "Failed to create CAN socket";
device.reset(QCanBus::instance()->createDevice("socketcan", config.device, &errorString));
device->setConfigurationParameter(QCanBusDevice::CanFdKey, true);
if (!device) {
qDebug() << "Failed to create SocketCAN device" << errorString;
return false; return false;
} }
if (!device->connectDevice()) { // Enable CAN-FD
qDebug() << "Failed to connect to device"; int fd_enable = 1;
setsockopt(sock_fd, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &fd_enable, sizeof(fd_enable));
struct ifreq ifr = {};
strncpy(ifr.ifr_name, config.device.c_str(), IFNAMSIZ - 1);
if (ioctl(sock_fd, SIOCGIFINDEX, &ifr) < 0) {
qDebug() << "Failed to get interface index for" << config.device.c_str();
::close(sock_fd);
sock_fd = -1;
return false; return false;
} }
struct sockaddr_can addr = {};
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
if (bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
qDebug() << "Failed to bind CAN socket";
::close(sock_fd);
sock_fd = -1;
return false;
}
// Set read timeout so the thread can check for interruption
struct timeval tv = {.tv_sec = 0, .tv_usec = 100000}; // 100ms
setsockopt(sock_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
return true; return true;
} }
void SocketCanStream::streamThread() { void SocketCanStream::streamThread() {
while (!QThread::currentThread()->isInterruptionRequested()) { struct canfd_frame frame;
QThread::msleep(1);
auto frames = device->readAllFrames(); while (!QThread::currentThread()->isInterruptionRequested()) {
if (frames.size() == 0) continue; ssize_t nbytes = read(sock_fd, &frame, sizeof(frame));
if (nbytes <= 0) continue;
uint8_t len = (nbytes == CAN_MTU) ? frame.len : frame.len; // works for both CAN and CAN-FD
MessageBuilder msg; MessageBuilder msg;
auto evt = msg.initEvent(); auto evt = msg.initEvent();
auto canData = evt.initCan(frames.size()); auto canData = evt.initCan(1);
canData[0].setAddress(frame.can_id & CAN_EFF_MASK);
for (uint i = 0; i < frames.size(); i++) { canData[0].setSrc(0);
if (!frames[i].isValid()) continue; canData[0].setDat(kj::arrayPtr(frame.data, len));
canData[i].setAddress(frames[i].frameId());
canData[i].setSrc(0);
auto payload = frames[i].payload();
canData[i].setDat(kj::arrayPtr((uint8_t*)payload.data(), payload.size()));
}
handleEvent(capnp::messageToFlatArray(msg)); handleEvent(capnp::messageToFlatArray(msg));
} }
@@ -87,7 +118,7 @@ OpenSocketCanWidget::OpenSocketCanWidget(QWidget *parent) : AbstractOpenStreamWi
main_layout->addStretch(1); main_layout->addStretch(1);
QObject::connect(refresh, &QPushButton::clicked, this, &OpenSocketCanWidget::refreshDevices); QObject::connect(refresh, &QPushButton::clicked, this, &OpenSocketCanWidget::refreshDevices);
QObject::connect(device_edit, &QComboBox::currentTextChanged, this, [=]{ config.device = device_edit->currentText(); }); QObject::connect(device_edit, &QComboBox::currentTextChanged, this, [=]{ config.device = device_edit->currentText().toStdString(); });
// Populate devices // Populate devices
refreshDevices(); refreshDevices();
@@ -95,12 +126,19 @@ OpenSocketCanWidget::OpenSocketCanWidget(QWidget *parent) : AbstractOpenStreamWi
void OpenSocketCanWidget::refreshDevices() { void OpenSocketCanWidget::refreshDevices() {
device_edit->clear(); device_edit->clear();
for (auto device : QCanBus::instance()->availableDevices(QStringLiteral("socketcan"))) { // Scan /sys/class/net/ for CAN interfaces (type 280 = ARPHRD_CAN)
device_edit->addItem(device.name()); QDir net_dir("/sys/class/net");
for (const auto &iface : net_dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
QFile type_file(net_dir.filePath(iface) + "/type");
if (type_file.open(QIODevice::ReadOnly)) {
int type = type_file.readAll().trimmed().toInt();
if (type == 280) {
device_edit->addItem(iface);
}
}
} }
} }
AbstractStream *OpenSocketCanWidget::open() { AbstractStream *OpenSocketCanWidget::open() {
try { try {
return new SocketCanStream(qApp, config); return new SocketCanStream(qApp, config);
+5 -10
View File
@@ -1,27 +1,22 @@
#pragma once #pragma once
#include <memory>
#include <QtSerialBus/QCanBus>
#include <QtSerialBus/QCanBusDevice>
#include <QtSerialBus/QCanBusDeviceInfo>
#include <QComboBox> #include <QComboBox>
#include "tools/cabana/streams/livestream.h" #include "tools/cabana/streams/livestream.h"
struct SocketCanStreamConfig { struct SocketCanStreamConfig {
QString device = ""; // TODO: support multiple devices/buses at once std::string device = ""; // TODO: support multiple devices/buses at once
}; };
class SocketCanStream : public LiveStream { class SocketCanStream : public LiveStream {
Q_OBJECT Q_OBJECT
public: public:
SocketCanStream(QObject *parent, SocketCanStreamConfig config_ = {}); SocketCanStream(QObject *parent, SocketCanStreamConfig config_ = {});
~SocketCanStream() { stop(); } ~SocketCanStream();
static bool available(); static bool available();
inline QString routeName() const override { inline std::string routeName() const override {
return QString("Live Streaming From Socket CAN %1").arg(config.device); return "Live Streaming From Socket CAN " + config.device;
} }
protected: protected:
@@ -29,7 +24,7 @@ protected:
bool connect(); bool connect();
SocketCanStreamConfig config = {}; SocketCanStreamConfig config = {};
std::unique_ptr<QCanBusDevice> device; int sock_fd = -1;
}; };
class OpenSocketCanWidget : public AbstractOpenStreamWidget { class OpenSocketCanWidget : public AbstractOpenStreamWidget {
+9 -9
View File
@@ -8,7 +8,7 @@
const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2";
TEST_CASE("DBCFile::generateDBC") { TEST_CASE("DBCFile::generateDBC") {
QString fn = QString("%1/%2.dbc").arg(OPENDBC_FILE_PATH, "tesla_can"); std::string fn = std::string(OPENDBC_FILE_PATH) + "/tesla_can.dbc";
DBCFile dbc_origin(fn); DBCFile dbc_origin(fn);
DBCFile dbc_from_generated("", dbc_origin.generateDBC()); DBCFile dbc_from_generated("", dbc_origin.generateDBC());
@@ -30,7 +30,7 @@ TEST_CASE("DBCFile::generateDBC") {
TEST_CASE("DBCFile::generateDBC - comment order") { TEST_CASE("DBCFile::generateDBC - comment order") {
// Ensure that message comments are followed by signal comments and in the correct order // Ensure that message comments are followed by signal comments and in the correct order
auto content = R"(BO_ 160 message_1: 8 EON std::string content = R"(BO_ 160 message_1: 8 EON
SG_ signal_1 : 0|12@1+ (1,0) [0|4095] "unit" XXX SG_ signal_1 : 0|12@1+ (1,0) [0|4095] "unit" XXX
BO_ 162 message_2: 8 EON BO_ 162 message_2: 8 EON
@@ -46,7 +46,7 @@ CM_ SG_ 162 signal_2 "signal comment";
} }
TEST_CASE("DBCFile::generateDBC -- preserve original header") { TEST_CASE("DBCFile::generateDBC -- preserve original header") {
QString content = R"(VERSION "1.0" std::string content = R"(VERSION "1.0"
NS_ : NS_ :
CM_ CM_
@@ -66,7 +66,7 @@ CM_ SG_ 160 signal_1 "signal comment";
} }
TEST_CASE("DBCFile::generateDBC - escaped quotes") { TEST_CASE("DBCFile::generateDBC - escaped quotes") {
QString content = R"(BO_ 160 message_1: 8 EON std::string content = R"(BO_ 160 message_1: 8 EON
SG_ signal_1 : 0|12@1+ (1,0) [0|4095] "unit" XXX SG_ signal_1 : 0|12@1+ (1,0) [0|4095] "unit" XXX
CM_ BO_ 160 "message comment with \"escaped quotes\""; CM_ BO_ 160 "message comment with \"escaped quotes\"";
@@ -77,7 +77,7 @@ CM_ SG_ 160 signal_1 "signal comment with \"escaped quotes\"";
} }
TEST_CASE("parse_dbc") { TEST_CASE("parse_dbc") {
QString content = R"( std::string content = R"(
BO_ 160 message_1: 8 EON BO_ 160 message_1: 8 EON
SG_ signal_1 : 0|12@1+ (1,0) [0|4095] "unit" XXX SG_ signal_1 : 0|12@1+ (1,0) [0|4095] "unit" XXX
SG_ signal_2 : 12|1@1+ (1.0,0.0) [0.0|1] "" XXX SG_ signal_2 : 12|1@1+ (1.0,0.0) [0.0|1] "" XXX
@@ -119,9 +119,9 @@ CM_ SG_ 162 signal_1 "signal comment with \"escaped quotes\"";
REQUIRE(sig_1->comment == "signal comment"); REQUIRE(sig_1->comment == "signal comment");
REQUIRE(sig_1->receiver_name == "XXX"); REQUIRE(sig_1->receiver_name == "XXX");
REQUIRE(sig_1->val_desc.size() == 3); REQUIRE(sig_1->val_desc.size() == 3);
REQUIRE(sig_1->val_desc[0] == std::pair<double, QString>{0, "disabled"}); REQUIRE(sig_1->val_desc[0] == std::pair<double, std::string>{0, "disabled"});
REQUIRE(sig_1->val_desc[1] == std::pair<double, QString>{1.2, "initializing"}); REQUIRE(sig_1->val_desc[1] == std::pair<double, std::string>{1.2, "initializing"});
REQUIRE(sig_1->val_desc[2] == std::pair<double, QString>{2, "fault"}); REQUIRE(sig_1->val_desc[2] == std::pair<double, std::string>{2, "fault"});
auto &sig_2 = msg->sigs[1]; auto &sig_2 = msg->sigs[1];
REQUIRE(sig_2->comment == "multiple line comment \n1\n2"); REQUIRE(sig_2->comment == "multiple line comment \n1\n2");
@@ -147,7 +147,7 @@ TEST_CASE("parse_opendbc") {
QStringList errors; QStringList errors;
for (auto fn : dir.entryList({"*.dbc"}, QDir::Files, QDir::Name)) { for (auto fn : dir.entryList({"*.dbc"}, QDir::Files, QDir::Name)) {
try { try {
auto dbc = DBCFile(dir.filePath(fn)); auto dbc = DBCFile(dir.filePath(fn).toStdString());
} catch (std::exception &e) { } catch (std::exception &e) {
errors.push_back(e.what()); errors.push_back(e.what());
} }
+40 -26
View File
@@ -1,10 +1,11 @@
#include "tools/cabana/tools/findsignal.h" #include "tools/cabana/tools/findsignal.h"
#include <thread>
#include <QFormLayout> #include <QFormLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QHeaderView> #include <QHeaderView>
#include <QMenu> #include <QMenu>
#include <QtConcurrent>
#include <QTimer> #include <QTimer>
#include <QVBoxLayout> #include <QVBoxLayout>
@@ -20,7 +21,7 @@ QVariant FindSignalModel::data(const QModelIndex &index, int role) const {
if (role == Qt::DisplayRole) { if (role == Qt::DisplayRole) {
const auto &s = filtered_signals[index.row()]; const auto &s = filtered_signals[index.row()];
switch (index.column()) { switch (index.column()) {
case 0: return s.id.toString(); case 0: return QString::fromStdString(s.id.toString());
case 1: return QString("%1, %2").arg(s.sig.start_bit).arg(s.sig.size); case 1: return QString("%1, %2").arg(s.sig.start_bit).arg(s.sig.size);
case 2: return s.values.join(" "); case 2: return s.values.join(" ");
} }
@@ -32,36 +33,49 @@ void FindSignalModel::search(std::function<bool(double)> cmp) {
beginResetModel(); beginResetModel();
std::mutex lock; std::mutex lock;
const auto prev_sigs = !histories.isEmpty() ? histories.back() : initial_signals; const auto prev_sigs = !histories.empty() ? histories.back() : initial_signals;
filtered_signals.clear(); filtered_signals.clear();
filtered_signals.reserve(prev_sigs.size()); filtered_signals.reserve(prev_sigs.size());
QtConcurrent::blockingMap(prev_sigs, [&](auto &s) {
const auto &events = can->events(s.id);
auto first = std::upper_bound(events.cbegin(), events.cend(), s.mono_time, CompareCanEvent());
auto last = events.cend();
if (last_time < std::numeric_limits<uint64_t>::max()) {
last = std::upper_bound(events.cbegin(), events.cend(), last_time, CompareCanEvent());
}
auto it = std::find_if(first, last, [&](const CanEvent *e) { return cmp(get_raw_value(e->dat, e->size, s.sig)); }); unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency());
if (it != last) { size_t chunk = (prev_sigs.size() + num_threads - 1) / num_threads;
auto values = s.values; std::vector<std::thread> threads;
values += QString("(%1, %2)").arg(can->toSeconds((*it)->mono_time), 0, 'f', 3).arg(get_raw_value((*it)->dat, (*it)->size, s.sig)); for (unsigned int t = 0; t < num_threads && t * chunk < (size_t)prev_sigs.size(); ++t) {
std::lock_guard lk(lock); size_t start = t * chunk;
filtered_signals.push_back({.id = s.id, .mono_time = (*it)->mono_time, .sig = s.sig, .values = values}); size_t end = std::min(start + chunk, (size_t)prev_sigs.size());
} threads.emplace_back([&, start, end]() {
}); for (size_t i = start; i < end; ++i) {
const auto &s = prev_sigs[i];
const auto &events = can->events(s.id);
auto first = std::upper_bound(events.cbegin(), events.cend(), s.mono_time, CompareCanEvent());
auto last = events.cend();
if (last_time < std::numeric_limits<uint64_t>::max()) {
last = std::upper_bound(events.cbegin(), events.cend(), last_time, CompareCanEvent());
}
auto it = std::find_if(first, last, [&](const CanEvent *e) { return cmp(get_raw_value(e->dat, e->size, s.sig)); });
if (it != last) {
auto values = s.values;
values += QString("(%1, %2)").arg(can->toSeconds((*it)->mono_time), 0, 'f', 3).arg(get_raw_value((*it)->dat, (*it)->size, s.sig));
std::lock_guard lk(lock);
filtered_signals.push_back({.id = s.id, .mono_time = (*it)->mono_time, .sig = s.sig, .values = values});
}
}
});
}
for (auto &th : threads) th.join();
histories.push_back(filtered_signals); histories.push_back(filtered_signals);
endResetModel(); endResetModel();
} }
void FindSignalModel::undo() { void FindSignalModel::undo() {
if (!histories.isEmpty()) { if (!histories.empty()) {
beginResetModel(); beginResetModel();
histories.pop_back(); histories.pop_back();
filtered_signals.clear(); filtered_signals.clear();
if (!histories.isEmpty()) filtered_signals = histories.back(); if (!histories.empty()) filtered_signals = histories.back();
endResetModel(); endResetModel();
} }
} }
@@ -172,7 +186,7 @@ FindSignalDlg::FindSignalDlg(QWidget *parent) : QDialog(parent, Qt::WindowFlags(
} }
void FindSignalDlg::search() { void FindSignalDlg::search() {
if (model->histories.isEmpty()) { if (model->histories.empty()) {
setInitialSignals(); setInitialSignals();
} }
auto v1 = value1->text().toDouble(); auto v1 = value1->text().toDouble();
@@ -246,12 +260,12 @@ void FindSignalDlg::setInitialSignals() {
} }
void FindSignalDlg::modelReset() { void FindSignalDlg::modelReset() {
properties_group->setEnabled(model->histories.isEmpty()); properties_group->setEnabled(model->histories.empty());
message_group->setEnabled(model->histories.isEmpty()); message_group->setEnabled(model->histories.empty());
search_btn->setText(model->histories.isEmpty() ? tr("Find") : tr("Find Next")); search_btn->setText(model->histories.empty() ? tr("Find") : tr("Find Next"));
reset_btn->setEnabled(!model->histories.isEmpty()); reset_btn->setEnabled(!model->histories.empty());
undo_btn->setEnabled(model->histories.size() > 1); undo_btn->setEnabled(model->histories.size() > 1);
search_btn->setEnabled(model->rowCount() > 0 || model->histories.isEmpty()); search_btn->setEnabled(model->rowCount() > 0 || model->histories.empty());
stats_label->setVisible(true); stats_label->setVisible(true);
stats_label->setText(tr("%1 matches. right click on an item to create signal. double click to open message").arg(model->filtered_signals.size())); stats_label->setText(tr("%1 matches. right click on an item to create signal. double click to open message").arg(model->filtered_signals.size()));
} }
+6 -4
View File
@@ -2,6 +2,8 @@
#include <algorithm> #include <algorithm>
#include <limits> #include <limits>
#include <string>
#include <vector>
#include <QAbstractTableModel> #include <QAbstractTableModel>
#include <QCheckBox> #include <QCheckBox>
@@ -26,14 +28,14 @@ public:
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 3; } int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 3; }
int rowCount(const QModelIndex &parent = QModelIndex()) const override { return std::min(filtered_signals.size(), 300); } int rowCount(const QModelIndex &parent = QModelIndex()) const override { return std::min((int)filtered_signals.size(), 300); }
void search(std::function<bool(double)> cmp); void search(std::function<bool(double)> cmp);
void reset(); void reset();
void undo(); void undo();
QList<SearchSignal> filtered_signals; std::vector<SearchSignal> filtered_signals;
QList<SearchSignal> initial_signals; std::vector<SearchSignal> initial_signals;
QList<QList<SearchSignal>> histories; std::vector<std::vector<SearchSignal>> histories;
uint64_t last_time = std::numeric_limits<uint64_t>::max(); uint64_t last_time = std::numeric_limits<uint64_t>::max();
}; };
+11 -10
View File
@@ -1,6 +1,7 @@
#include "tools/cabana/tools/findsimilarbits.h" #include "tools/cabana/tools/findsimilarbits.h"
#include <algorithm> #include <algorithm>
#include <unordered_map>
#include <QGridLayout> #include <QGridLayout>
#include <QHeaderView> #include <QHeaderView>
@@ -31,7 +32,7 @@ FindSimilarBitsDlg::FindSimilarBitsDlg(QWidget *parent) : QDialog(parent, Qt::Wi
msg_cb = new QComboBox(this); msg_cb = new QComboBox(this);
// TODO: update when src_bus_combo changes // TODO: update when src_bus_combo changes
for (auto &[address, msg] : dbc()->getMessages(-1)) { for (auto &[address, msg] : dbc()->getMessages(-1)) {
msg_cb->addItem(msg.name, address); msg_cb->addItem(QString::fromStdString(msg.name), address);
} }
msg_cb->model()->sort(0); msg_cb->model()->sort(0);
msg_cb->setCurrentIndex(0); msg_cb->setCurrentIndex(0);
@@ -114,10 +115,10 @@ void FindSimilarBitsDlg::find() {
search_btn->setEnabled(true); search_btn->setEnabled(true);
} }
QList<FindSimilarBitsDlg::mismatched_struct> FindSimilarBitsDlg::calcBits(uint8_t bus, uint32_t selected_address, int byte_idx, std::vector<FindSimilarBitsDlg::mismatched_struct> FindSimilarBitsDlg::calcBits(uint8_t bus, uint32_t selected_address, int byte_idx,
int bit_idx, uint8_t find_bus, bool equal, int min_msgs_cnt) { int bit_idx, uint8_t find_bus, bool equal, int min_msgs_cnt) {
QHash<uint32_t, QVector<uint32_t>> mismatches; std::unordered_map<uint32_t, std::vector<uint32_t>> mismatches;
QHash<uint32_t, uint32_t> msg_count; std::unordered_map<uint32_t, uint32_t> msg_count;
const auto &events = can->allEvents(); const auto &events = can->allEvents();
int bit_to_find = -1; int bit_to_find = -1;
for (const CanEvent *e : events) { for (const CanEvent *e : events) {
@@ -143,14 +144,14 @@ QList<FindSimilarBitsDlg::mismatched_struct> FindSimilarBitsDlg::calcBits(uint8_
} }
} }
QList<mismatched_struct> result; std::vector<mismatched_struct> result;
result.reserve(mismatches.size()); result.reserve(mismatches.size());
for (auto it = mismatches.begin(); it != mismatches.end(); ++it) { for (auto it = mismatches.begin(); it != mismatches.end(); ++it) {
if (auto cnt = msg_count[it.key()]; cnt > min_msgs_cnt) { if (auto cnt = msg_count[it->first]; cnt > (uint32_t)min_msgs_cnt) {
auto &mismatched = it.value(); auto &mismatched = it->second;
for (int i = 0; i < mismatched.size(); ++i) { for (int i = 0; i < (int)mismatched.size(); ++i) {
if (float perc = (mismatched[i] / (double)cnt) * 100; perc < 50) { if (float perc = (mismatched[i] / (double)cnt) * 100; perc < 50) {
result.push_back({it.key(), (uint32_t)i / 8, (uint32_t)i % 8, mismatched[i], cnt, perc}); result.push_back({it->first, (uint32_t)i / 8, (uint32_t)i % 8, mismatched[i], cnt, perc});
} }
} }
} }
+3 -1
View File
@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <vector>
#include <QComboBox> #include <QComboBox>
#include <QDialog> #include <QDialog>
#include <QLineEdit> #include <QLineEdit>
@@ -22,7 +24,7 @@ private:
uint32_t address, byte_idx, bit_idx, mismatches, total; uint32_t address, byte_idx, bit_idx, mismatches, total;
float perc; float perc;
}; };
QList<mismatched_struct> calcBits(uint8_t bus, uint32_t selected_address, int byte_idx, int bit_idx, uint8_t find_bus, std::vector<mismatched_struct> calcBits(uint8_t bus, uint32_t selected_address, int byte_idx, int bit_idx, uint8_t find_bus,
bool equal, int min_msgs_cnt); bool equal, int min_msgs_cnt);
void find(); void find();
+1 -1
View File
@@ -26,7 +26,7 @@ void exportSignalsToCSV(const QString &file_name, const MessageId &msg_id) {
QTextStream stream(&file); QTextStream stream(&file);
stream << "time,addr,bus"; stream << "time,addr,bus";
for (auto s : msg->sigs) for (auto s : msg->sigs)
stream << "," << s->name; stream << "," << s->name.c_str();
stream << "\n"; stream << "\n";
for (auto e : can->events(msg_id)) { for (auto e : can->events(msg_id)) {
+33 -21
View File
@@ -17,8 +17,7 @@
#include <QSurfaceFormat> #include <QSurfaceFormat>
#include <QFileInfo> #include <QFileInfo>
#include <QPainterPath> #include <QPainterPath>
#include <QTextStream> #include <unordered_map>
#include <QtXml/QDomDocument>
#include "common/util.h" #include "common/util.h"
// SegmentTree // SegmentTree
@@ -278,7 +277,7 @@ QString signalToolTip(const cabana::Signal *sig) {
Start Bit: %2 Size: %3<br /> Start Bit: %2 Size: %3<br />
MSB: %4 LSB: %5<br /> MSB: %4 LSB: %5<br />
Little Endian: %6 Signed: %7</span> Little Endian: %6 Signed: %7</span>
)").arg(sig->name).arg(sig->start_bit).arg(sig->size).arg(sig->msb).arg(sig->lsb) )").arg(QString::fromStdString(sig->name)).arg(sig->start_bit).arg(sig->size).arg(sig->msb).arg(sig->lsb)
.arg(sig->is_little_endian ? "Y" : "N").arg(sig->is_signed ? "Y" : "N"); .arg(sig->is_little_endian ? "Y" : "N").arg(sig->is_signed ? "Y" : "N");
} }
@@ -325,36 +324,49 @@ void initApp(int argc, char *argv[], bool disable_hidpi) {
setSurfaceFormat(); setSurfaceFormat();
} }
static QHash<QString, QByteArray> load_bootstrap_icons() { static std::unordered_map<std::string, std::string> load_bootstrap_icons() {
QHash<QString, QByteArray> icons; std::unordered_map<std::string, std::string> icons;
QFile f(":/bootstrap-icons.svg"); QFile f(":/bootstrap-icons.svg");
if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { if (f.open(QIODevice::ReadOnly | QIODevice::Text)) {
QDomDocument xml; std::string content = f.readAll().toStdString();
xml.setContent(&f); const std::string sym_open = "<symbol ";
QDomNode n = xml.documentElement().firstChild(); const std::string sym_close = "</symbol>";
while (!n.isNull()) { const std::string id_attr = "id=\"";
QDomElement e = n.toElement();
if (!e.isNull() && e.hasAttribute("id")) { size_t pos = 0;
QString svg_str; while ((pos = content.find(sym_open, pos)) != std::string::npos) {
QTextStream stream(&svg_str); size_t end = content.find(sym_close, pos);
n.save(stream, 0); if (end == std::string::npos) break;
svg_str.replace("<symbol", "<svg"); end += sym_close.size();
svg_str.replace("</symbol>", "</svg>");
icons[e.attribute("id")] = svg_str.toUtf8(); // extract id
size_t id_start = content.find(id_attr, pos);
if (id_start != std::string::npos && id_start < end) {
id_start += id_attr.size();
size_t id_end = content.find('"', id_start);
if (id_end != std::string::npos && id_end < end) {
std::string id = content.substr(id_start, id_end - id_start);
std::string svg_str = content.substr(pos, end - pos);
// replace <symbol with <svg, </symbol> with </svg>
svg_str.replace(0, 7, "<svg"); // "<symbol" (7) -> "<svg" (4)
svg_str.replace(svg_str.size() - 9, 9, "</svg>"); // "</symbol>" (9) -> "</svg>" (6)
icons[id] = std::move(svg_str);
}
} }
n = n.nextSibling(); pos = end;
} }
} }
return icons; return icons;
} }
QPixmap bootstrapPixmap(const QString &id) { QPixmap bootstrapPixmap(const QString &id) {
static QHash<QString, QByteArray> icons = load_bootstrap_icons(); static auto icons = load_bootstrap_icons();
QPixmap pixmap; QPixmap pixmap;
if (auto it = icons.find(id); it != icons.end()) { auto it = icons.find(id.toStdString());
pixmap.loadFromData(it.value(), "svg"); if (it != icons.end()) {
pixmap.loadFromData((const uchar *)it->second.data(), it->second.size(), "svg");
} }
return pixmap; return pixmap;
} }
+29 -17
View File
@@ -1,6 +1,7 @@
#include "tools/cabana/videowidget.h" #include "tools/cabana/videowidget.h"
#include <algorithm> #include <algorithm>
#include <thread>
#include <QAction> #include <QAction>
#include <QActionGroup> #include <QActionGroup>
@@ -9,7 +10,6 @@
#include <QPainter> #include <QPainter>
#include <QStyleOptionSlider> #include <QStyleOptionSlider>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QtConcurrent>
#include "tools/cabana/tools/routeinfo.h" #include "tools/cabana/tools/routeinfo.h"
@@ -334,19 +334,31 @@ StreamCameraView::StreamCameraView(std::string stream_name, VisionStreamType str
void StreamCameraView::parseQLog(std::shared_ptr<LogReader> qlog) { void StreamCameraView::parseQLog(std::shared_ptr<LogReader> qlog) {
std::mutex mutex; std::mutex mutex;
QtConcurrent::blockingMap(qlog->events.cbegin(), qlog->events.cend(), [this, &mutex](const Event &e) { const auto &events = qlog->events;
if (e.which == cereal::Event::Which::THUMBNAIL) { unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency());
capnp::FlatArrayMessageReader reader(e.data); size_t chunk = (events.size() + num_threads - 1) / num_threads;
auto thumb_data = reader.getRoot<cereal::Event>().getThumbnail(); std::vector<std::thread> threads;
auto image_data = thumb_data.getThumbnail(); for (unsigned int t = 0; t < num_threads && t * chunk < events.size(); ++t) {
if (QPixmap thumb; thumb.loadFromData(image_data.begin(), image_data.size(), "jpeg")) { size_t start = t * chunk;
QPixmap generated_thumb = generateThumbnail(thumb, can->toSeconds(thumb_data.getTimestampEof())); size_t end = std::min(start + chunk, events.size());
std::lock_guard lock(mutex); threads.emplace_back([this, &mutex, &events, start, end]() {
thumbnails[thumb_data.getTimestampEof()] = generated_thumb; for (size_t i = start; i < end; ++i) {
big_thumbnails[thumb_data.getTimestampEof()] = thumb; const Event &e = events[i];
if (e.which == cereal::Event::Which::THUMBNAIL) {
capnp::FlatArrayMessageReader reader(e.data);
auto thumb_data = reader.getRoot<cereal::Event>().getThumbnail();
auto image_data = thumb_data.getThumbnail();
if (QPixmap thumb; thumb.loadFromData(image_data.begin(), image_data.size(), "jpeg")) {
QPixmap generated_thumb = generateThumbnail(thumb, can->toSeconds(thumb_data.getTimestampEof()));
std::lock_guard lock(mutex);
thumbnails[thumb_data.getTimestampEof()] = generated_thumb;
big_thumbnails[thumb_data.getTimestampEof()] = thumb;
}
}
} }
} });
}); }
for (auto &th : threads) th.join();
update(); update();
} }
@@ -384,9 +396,9 @@ QPixmap StreamCameraView::generateThumbnail(QPixmap thumb, double seconds) {
void StreamCameraView::drawScrubThumbnail(QPainter &p) { void StreamCameraView::drawScrubThumbnail(QPainter &p) {
p.fillRect(rect(), Qt::black); p.fillRect(rect(), Qt::black);
auto it = big_thumbnails.lowerBound(can->toMonoTime(thumbnail_dispaly_time)); auto it = big_thumbnails.lower_bound(can->toMonoTime(thumbnail_dispaly_time));
if (it != big_thumbnails.end()) { if (it != big_thumbnails.end()) {
QPixmap scaled_thumb = it.value().scaled(rect().size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); QPixmap scaled_thumb = it->second.scaled(rect().size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
QRect thumb_rect(rect().center() - scaled_thumb.rect().center(), scaled_thumb.size()); QRect thumb_rect(rect().center() - scaled_thumb.rect().center(), scaled_thumb.size());
p.drawPixmap(thumb_rect.topLeft(), scaled_thumb); p.drawPixmap(thumb_rect.topLeft(), scaled_thumb);
drawTime(p, thumb_rect, thumbnail_dispaly_time); drawTime(p, thumb_rect, thumbnail_dispaly_time);
@@ -394,9 +406,9 @@ void StreamCameraView::drawScrubThumbnail(QPainter &p) {
} }
void StreamCameraView::drawThumbnail(QPainter &p) { void StreamCameraView::drawThumbnail(QPainter &p) {
auto it = thumbnails.lowerBound(can->toMonoTime(thumbnail_dispaly_time)); auto it = thumbnails.lower_bound(can->toMonoTime(thumbnail_dispaly_time));
if (it != thumbnails.end()) { if (it != thumbnails.end()) {
const QPixmap &thumb = it.value(); const QPixmap &thumb = it->second;
auto [min_sec, max_sec] = can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds())); auto [min_sec, max_sec] = can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds()));
int pos = (thumbnail_dispaly_time - min_sec) * width() / (max_sec - min_sec); int pos = (thumbnail_dispaly_time - min_sec) * width() / (max_sec - min_sec);
int x = std::clamp(pos - thumb.width() / 2, THUMBNAIL_MARGIN, width() - thumb.width() - THUMBNAIL_MARGIN + 1); int x = std::clamp(pos - thumb.width() / 2, THUMBNAIL_MARGIN, width() - thumb.width() - THUMBNAIL_MARGIN + 1);
+3 -2
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <map>
#include <memory> #include <memory>
#include <set> #include <set>
#include <string> #include <string>
@@ -47,8 +48,8 @@ private:
void drawTime(QPainter &p, const QRect &rect, double seconds); void drawTime(QPainter &p, const QRect &rect, double seconds);
QPropertyAnimation *fade_animation; QPropertyAnimation *fade_animation;
QMap<uint64_t, QPixmap> big_thumbnails; std::map<uint64_t, QPixmap> big_thumbnails;
QMap<uint64_t, QPixmap> thumbnails; std::map<uint64_t, QPixmap> thumbnails;
double thumbnail_dispaly_time = -1; double thumbnail_dispaly_time = -1;
friend class VideoWidget; friend class VideoWidget;
}; };
-67
View File
@@ -1,67 +0,0 @@
# JotPluggler
JotPluggler is a tool to quickly visualize openpilot logs.
## Usage
```
$ ./jotpluggler/pluggle.py -h
usage: pluggle.py [-h] [--demo] [--layout LAYOUT] [route]
A tool for visualizing openpilot logs.
positional arguments:
route Optional route name to load on startup.
options:
-h, --help show this help message and exit
--demo Use the demo route instead of providing one
--layout LAYOUT Path to YAML layout file to load on startup
```
Example using route name:
`./pluggle.py "5beb9b58bd12b691/0000010a--a51155e496"`
Examples using segment:
`./pluggle.py "5beb9b58bd12b691/0000010a--a51155e496/1"`
`./pluggle.py "5beb9b58bd12b691/0000010a--a51155e496/1/q" # use qlogs`
Example using segment range:
`./pluggle.py "5beb9b58bd12b691/0000010a--a51155e496/0:1"`
## Demo
For a quick demo, run this command:
`./pluggle.py --demo --layout=layouts/torque-controller.yaml`
## Basic Usage/Features:
- The text box to load a route is a the top left of the page, accepts standard openpilot format routes (e.g. `5beb9b58bd12b691/0000010a--a51155e496/0:1`, `https://connect.comma.ai/5beb9b58bd12b691/0000010a--a51155e496/`)
- The Play/Pause button is at the bottom of the screen, you can drag the bottom slider to seek. The timeline in timeseries plots are synced with the slider.
- The Timeseries List sidebar has several dropdowns, the fields each show the field name and value, synced with the timeline (will show N/A until the time of the first message in that field is reached).
- There is a search bar for the timeseries list, you can search for structs or fields, or both by separating with a "/"
- You can drag and drop any numeric/boolean field from the timeseries list into a timeseries panel.
- You can create more panels with the split buttons (buttons with two rectangles, either horizontal or vertical). You can resize the panels by dragging the grip in between any panel.
- You can load and save layouts with the corresponding buttons. Layouts will save all tabs, panels, titles, timeseries, etc.
## Layouts
If you create a layout that's useful for others, consider upstreaming it.
## Plot Interaction Controls
- **Left click and drag within the plot area** to pan X
- Left click and drag on an axis to pan an individual axis (disabled for Y-axis)
- **Scroll in the plot area** to zoom in X axes, Y-axis is autofit
- Scroll on an axis to zoom an individual axis
- **Right click and drag** to select data and zoom into the selected data
- Left click while box selecting to cancel the selection
- **Double left click** to fit all visible data
- Double left click on an axis to fit the individual axis (disabled for Y-axis, always autofit)
- **Double right click** to open the plot context menu
- **Click legend label icons** to show/hide plot items
-3
View File
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3ea96d8193eb9067a5efdc5d88a3099730ecafa40efcd09d7402bb3efd723603
size 2305
-3
View File
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:53097ac5403b725ff1841dfa186ea770b4bb3714205824bde36ec3c2a0fb5dba
size 2758
-3
View File
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:248b71eafd1b42b0861da92114da3d625221cd88121fff01e0514bf3d79ff3b1
size 1364
-3
View File
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:54dd035ff898d881509fa686c402a61af8ef5fb408b92414722da01f773b0d33
size 2900
-3
View File
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:adbd4e5df1f58694dca9dde46d1d95b4e7471684e42e6bca9f41ea5d346e67c5
size 3669
-3
View File
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a6d9c90cb0dd906e0b15e1f7f3fd9f0dfad3c3b0b34eeed7a7882768dc5f3961
size 2053
-360
View File
@@ -1,360 +0,0 @@
import numpy as np
import threading
import multiprocessing
import bisect
from collections import defaultdict
from tqdm import tqdm
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.test.process_replay.migration import migrate_all
from openpilot.tools.lib.logreader import _LogFileReader, LogReader
def flatten_dict(d: dict, sep: str = "/", prefix: str | None = None) -> dict:
result = {}
stack: list[tuple] = [(d, prefix)]
while stack:
obj, current_prefix = stack.pop()
if isinstance(obj, dict):
for key, val in obj.items():
new_prefix = key if current_prefix is None else f"{current_prefix}{sep}{key}"
if isinstance(val, (dict, list)):
stack.append((val, new_prefix))
else:
result[new_prefix] = val
elif isinstance(obj, list):
for i, item in enumerate(obj):
new_prefix = f"{current_prefix}{sep}{i}"
if isinstance(item, (dict, list)):
stack.append((item, new_prefix))
else:
result[new_prefix] = item
else:
if current_prefix is not None:
result[current_prefix] = obj
return result
def extract_field_types(schema, prefix, field_types_dict):
stack = [(schema, prefix)]
while stack:
current_schema, current_prefix = stack.pop()
for field in current_schema.fields_list:
field_name = field.proto.name
field_path = f"{current_prefix}/{field_name}"
field_proto = field.proto
field_which = field_proto.which()
field_type = field_proto.slot.type.which() if field_which == 'slot' else field_which
field_types_dict[field_path] = field_type
if field_which == 'slot':
slot_type = field_proto.slot.type
type_which = slot_type.which()
if type_which == 'list':
element_type = slot_type.list.elementType.which()
list_path = f"{field_path}/*"
field_types_dict[list_path] = element_type
if element_type == 'struct':
stack.append((field.schema.elementType, list_path))
elif type_which == 'struct':
stack.append((field.schema, field_path))
elif field_which == 'group':
stack.append((field.schema, field_path))
def _convert_to_optimal_dtype(values_list, capnp_type):
dtype_mapping = {
'bool': np.bool_, 'int8': np.int8, 'int16': np.int16, 'int32': np.int32, 'int64': np.int64,
'uint8': np.uint8, 'uint16': np.uint16, 'uint32': np.uint32, 'uint64': np.uint64,
'float32': np.float32, 'float64': np.float64, 'text': object, 'data': object,
'enum': object, 'anyPointer': object,
}
target_dtype = dtype_mapping.get(capnp_type, object)
return np.array(values_list, dtype=target_dtype)
def _match_field_type(field_path, field_types):
if field_path in field_types:
return field_types[field_path]
path_parts = field_path.split('/')
template_parts = [p if not p.isdigit() else '*' for p in path_parts]
template_path = '/'.join(template_parts)
return field_types.get(template_path)
def _get_field_times_values(segment, field_name):
if field_name not in segment:
return None, None
field_data = segment[field_name]
segment_times = segment['t']
if field_data['sparse']:
if len(field_data['t_index']) == 0:
return None, None
return segment_times[field_data['t_index']], field_data['values']
else:
return segment_times, field_data['values']
def msgs_to_time_series(msgs):
"""Extract scalar fields and return (time_series_data, start_time, end_time)."""
collected_data = defaultdict(lambda: {'timestamps': [], 'columns': defaultdict(list), 'sparse_fields': set()})
field_types = {}
extracted_schemas = set()
min_time = max_time = None
for msg in msgs:
typ = msg.which()
timestamp = msg.logMonoTime * 1e-9
if typ != 'initData':
if min_time is None:
min_time = timestamp
max_time = timestamp
sub_msg = getattr(msg, typ)
if not hasattr(sub_msg, 'to_dict'):
continue
if hasattr(sub_msg, 'schema') and typ not in extracted_schemas:
extract_field_types(sub_msg.schema, typ, field_types)
extracted_schemas.add(typ)
try:
msg_dict = sub_msg.to_dict(verbose=True)
except Exception as e:
cloudlog.warning(f"Failed to convert sub_msg.to_dict() for message of type: {typ}: {e}")
continue
flat_dict = flatten_dict(msg_dict)
flat_dict['_valid'] = msg.valid
field_types[f"{typ}/_valid"] = 'bool'
type_data = collected_data[typ]
columns, sparse_fields = type_data['columns'], type_data['sparse_fields']
known_fields = set(columns.keys())
missing_fields = known_fields - flat_dict.keys()
for field, value in flat_dict.items():
if field not in known_fields and type_data['timestamps']:
sparse_fields.add(field)
columns[field].append(value)
if value is None:
sparse_fields.add(field)
for field in missing_fields:
columns[field].append(None)
sparse_fields.add(field)
type_data['timestamps'].append(timestamp)
final_result = {}
for typ, data in collected_data.items():
if not data['timestamps']:
continue
typ_result = {'t': np.array(data['timestamps'], dtype=np.float64)}
sparse_fields = data['sparse_fields']
for field_name, values in data['columns'].items():
if len(values) < len(data['timestamps']):
values = [None] * (len(data['timestamps']) - len(values)) + values
sparse_fields.add(field_name)
capnp_type = _match_field_type(f"{typ}/{field_name}", field_types)
if field_name in sparse_fields: # extract non-None values and their indices
non_none_indices = []
non_none_values = []
for i, value in enumerate(values):
if value is not None:
non_none_indices.append(i)
non_none_values.append(value)
if non_none_values: # check if indices > uint16 max, currently would require a 1000+ Hz signal since indices are within segments
assert max(non_none_indices) <= 65535, f"Sparse field {typ}/{field_name} has timestamp indices exceeding uint16 max. Max: {max(non_none_indices)}"
typ_result[field_name] = {
'values': _convert_to_optimal_dtype(non_none_values, capnp_type),
'sparse': True,
't_index': np.array(non_none_indices, dtype=np.uint16),
}
else: # dense representation
typ_result[field_name] = {'values': _convert_to_optimal_dtype(values, capnp_type), 'sparse': False}
final_result[typ] = typ_result
return final_result, min_time or 0.0, max_time or 0.0
def _process_segment(segment_identifier: str):
try:
lr = _LogFileReader(segment_identifier, sort_by_time=True)
migrated_msgs = migrate_all(lr)
return msgs_to_time_series(migrated_msgs)
except Exception as e:
cloudlog.warning(f"Warning: Failed to process segment {segment_identifier}: {e}")
return {}, 0.0, 0.0
class DataManager:
def __init__(self):
self._segments = []
self._segment_starts = []
self._start_time = 0.0
self._duration = 0.0
self._paths = set()
self._observers = []
self._loading = False
self._lock = threading.RLock()
def load_route(self, route: str) -> None:
if self._loading:
return
self._reset()
threading.Thread(target=self._load_async, args=(route,), daemon=True).start()
def get_timeseries(self, path: str):
with self._lock:
msg_type, field = path.split('/', 1)
times, values = [], []
for segment in self._segments:
if msg_type in segment:
field_times, field_values = _get_field_times_values(segment[msg_type], field)
if field_times is not None:
times.append(field_times)
values.append(field_values)
if not times:
return np.array([]), np.array([])
combined_times = np.concatenate(times) - self._start_time
if len(values) > 1:
first_dtype = values[0].dtype
if all(arr.dtype == first_dtype for arr in values): # check if all arrays have compatible dtypes
combined_values = np.concatenate(values)
else:
combined_values = np.concatenate([arr.astype(object) for arr in values])
else:
combined_values = values[0] if values else np.array([])
return combined_times, combined_values
def get_value_at(self, path: str, time: float):
with self._lock:
MAX_LOOKBACK = 5.0 # seconds
absolute_time = self._start_time + time
message_type, field = path.split('/', 1)
current_index = bisect.bisect_right(self._segment_starts, absolute_time) - 1
for index in (current_index, current_index - 1):
if not 0 <= index < len(self._segments):
continue
segment = self._segments[index].get(message_type)
if not segment:
continue
times, values = _get_field_times_values(segment, field)
if times is None or len(times) == 0 or (index != current_index and absolute_time - times[-1] > MAX_LOOKBACK):
continue
position = np.searchsorted(times, absolute_time, 'right') - 1
if position >= 0 and absolute_time - times[position] <= MAX_LOOKBACK:
return values[position]
return None
def get_all_paths(self):
with self._lock:
return sorted(self._paths)
def get_duration(self):
with self._lock:
return self._duration
def is_plottable(self, path: str):
_, values = self.get_timeseries(path)
if len(values) == 0:
return False
return np.issubdtype(values.dtype, np.number) or np.issubdtype(values.dtype, np.bool_)
def add_observer(self, callback):
with self._lock:
self._observers.append(callback)
def remove_observer(self, callback):
with self._lock:
if callback in self._observers:
self._observers.remove(callback)
def _reset(self):
with self._lock:
self._loading = True
self._segments.clear()
self._segment_starts.clear()
self._paths.clear()
self._start_time = self._duration = 0.0
observers = self._observers.copy()
for callback in observers:
callback({'reset': True})
def _load_async(self, route: str):
try:
lr = LogReader(route, sort_by_time=True)
if not lr.logreader_identifiers:
cloudlog.warning(f"Warning: No log segments found for route: {route}")
return
total_segments = len(lr.logreader_identifiers)
with self._lock:
observers = self._observers.copy()
for callback in observers:
callback({'metadata_loaded': True, 'total_segments': total_segments})
num_processes = max(1, multiprocessing.cpu_count() // 2)
with multiprocessing.Pool(processes=num_processes) as pool, tqdm(total=len(lr.logreader_identifiers), desc="Processing Segments") as pbar:
for segment_result, start_time, end_time in pool.imap(_process_segment, lr.logreader_identifiers):
pbar.update(1)
if segment_result:
self._add_segment(segment_result, start_time, end_time)
except Exception:
cloudlog.exception(f"Error loading route {route}:")
finally:
self._finalize_loading()
def _add_segment(self, segment_data: dict, start_time: float, end_time: float):
with self._lock:
self._segments.append(segment_data)
self._segment_starts.append(start_time)
if len(self._segments) == 1:
self._start_time = start_time
self._duration = end_time - self._start_time
for msg_type, data in segment_data.items():
for field_name in data.keys():
if field_name != 't':
self._paths.add(f"{msg_type}/{field_name}")
observers = self._observers.copy()
for callback in observers:
callback({'segment_added': True, 'duration': self._duration, 'segment_count': len(self._segments)})
def _finalize_loading(self):
with self._lock:
self._loading = False
observers = self._observers.copy()
duration = self._duration
for callback in observers:
callback({'loading_complete': True, 'duration': duration})
-315
View File
@@ -1,315 +0,0 @@
import os
import re
import threading
import numpy as np
import dearpygui.dearpygui as dpg
class DataTreeNode:
def __init__(self, name: str, full_path: str = "", parent=None):
self.name = name
self.full_path = full_path
self.parent = parent
self.children: dict[str, DataTreeNode] = {}
self.filtered_children: dict[str, DataTreeNode] = {}
self.created_children: dict[str, DataTreeNode] = {}
self.is_leaf = False
self.is_plottable: bool | None = None
self.ui_created = False
self.children_ui_created = False
self.ui_tag: str | None = None
class DataTree:
MAX_NODES_PER_FRAME = 50
def __init__(self, data_manager, playback_manager):
self.data_manager = data_manager
self.playback_manager = playback_manager
self.current_search = ""
self.data_tree = DataTreeNode(name="root")
self._build_queue: dict[str, tuple[DataTreeNode, DataTreeNode, str | int]] = {} # full_path -> (node, parent, before_tag)
self._current_created_paths: set[str] = set()
self._current_filtered_paths: set[str] = set()
self._path_to_node: dict[str, DataTreeNode] = {} # full_path -> node
self._expanded_tags: set[str] = set()
self._item_handlers: dict[str, str] = {} # ui_tag -> handler_tag
self._char_width = None
self._queued_search = None
self._new_data = False
self._ui_lock = threading.RLock()
self._handlers_to_delete = []
self.data_manager.add_observer(self._on_data_loaded)
def create_ui(self, parent_tag: str):
with dpg.child_window(parent=parent_tag, border=False, width=-1, height=-1):
dpg.add_text("Timeseries List")
dpg.add_separator()
dpg.add_input_text(tag="search_input", width=-1, hint="Search fields...", callback=self.search_data)
dpg.add_separator()
with dpg.child_window(border=False, width=-1, height=-1):
with dpg.group(tag="data_tree_container"):
pass
def _on_data_loaded(self, data: dict):
with self._ui_lock:
if data.get('segment_added') or data.get('reset'):
self._new_data = True
def update_frame(self, font):
if self._handlers_to_delete: # we need to do everything in main thread, frame callbacks are flaky
dpg.render_dearpygui_frame() # wait a frame to ensure queued callbacks are done
with self._ui_lock:
for handler in self._handlers_to_delete:
dpg.delete_item(handler)
self._handlers_to_delete.clear()
with self._ui_lock:
if self._char_width is None:
if size := dpg.get_text_size(" ", font=font):
self._char_width = size[0] / 2 # we scale font 2x and downscale to fix hidpi bug
if self._new_data:
self._process_path_change()
self._new_data = False
return
if self._queued_search is not None:
self.current_search = self._queued_search
self._process_path_change()
self._queued_search = None
return
nodes_processed = 0
while self._build_queue and nodes_processed < self.MAX_NODES_PER_FRAME:
child_node, parent, before_tag = self._build_queue.pop(next(iter(self._build_queue)))
parent_tag = "data_tree_container" if parent.name == "root" else parent.ui_tag
if not child_node.ui_created:
if child_node.is_leaf:
self._create_leaf_ui(child_node, parent_tag, before_tag)
else:
self._create_tree_node_ui(child_node, parent_tag, before_tag)
parent.created_children[child_node.name] = parent.children[child_node.name]
self._current_created_paths.add(child_node.full_path)
nodes_processed += 1
def _process_path_change(self):
self._build_queue.clear()
search_term = self.current_search.strip().lower()
all_paths = set(self.data_manager.get_all_paths())
new_filtered_leafs = {path for path in all_paths if self._should_show_path(path, search_term)}
new_filtered_paths = set(new_filtered_leafs)
for path in new_filtered_leafs:
parts = path.split('/')
for i in range(1, len(parts)):
prefix = '/'.join(parts[:i])
new_filtered_paths.add(prefix)
created_paths_to_remove = self._current_created_paths - new_filtered_paths
filtered_paths_to_remove = self._current_filtered_paths - new_filtered_leafs
if created_paths_to_remove or filtered_paths_to_remove:
self._remove_paths_from_tree(created_paths_to_remove, filtered_paths_to_remove)
self._apply_expansion_to_tree(self.data_tree, search_term)
paths_to_add = new_filtered_leafs - self._current_created_paths
if paths_to_add:
self._add_paths_to_tree(paths_to_add)
self._apply_expansion_to_tree(self.data_tree, search_term)
self._current_filtered_paths = new_filtered_paths
def _remove_paths_from_tree(self, created_paths_to_remove, filtered_paths_to_remove):
for path in sorted(created_paths_to_remove, reverse=True):
current_node = self._path_to_node[path]
if len(current_node.created_children) == 0:
self._current_created_paths.remove(current_node.full_path)
if item_handler_tag := self._item_handlers.get(current_node.ui_tag):
dpg.configure_item(item_handler_tag, show=False)
self._handlers_to_delete.append(item_handler_tag)
del self._item_handlers[current_node.ui_tag]
dpg.delete_item(current_node.ui_tag)
current_node.ui_created = False
current_node.ui_tag = None
current_node.children_ui_created = False
del current_node.parent.created_children[current_node.name]
del current_node.parent.filtered_children[current_node.name]
for path in filtered_paths_to_remove:
parts = path.split('/')
current_node = self._path_to_node[path]
part_array_index = -1
while len(current_node.filtered_children) == 0 and part_array_index >= -len(parts):
current_node = current_node.parent
if parts[part_array_index] in current_node.filtered_children:
del current_node.filtered_children[parts[part_array_index]]
part_array_index -= 1
def _add_paths_to_tree(self, paths):
parent_nodes_to_recheck = set()
for path in sorted(paths):
parts = path.split('/')
current_node = self.data_tree
current_path_prefix = ""
for i, part in enumerate(parts):
current_path_prefix = f"{current_path_prefix}/{part}" if current_path_prefix else part
if i < len(parts):
parent_nodes_to_recheck.add(current_node) # for incremental changes from new data
if part not in current_node.children:
current_node.children[part] = DataTreeNode(name=part, full_path=current_path_prefix, parent=current_node)
self._path_to_node[current_path_prefix] = current_node.children[part]
current_node.filtered_children[part] = current_node.children[part]
current_node = current_node.children[part]
if not current_node.is_leaf:
current_node.is_leaf = True
for p_node in parent_nodes_to_recheck:
p_node.children_ui_created = False
self._request_children_build(p_node)
def _get_node_label_and_expand(self, node: DataTreeNode, search_term: str):
label = f"{node.name} ({len(node.filtered_children)} fields)"
expand = len(search_term) > 0 and any(search_term in path for path in self._get_descendant_paths(node))
if expand and node.parent and len(node.parent.filtered_children) > 100 and len(node.filtered_children) > 2:
label += " (+)" # symbol for large lists which aren't fully expanded for performance (only affects procLog rn)
expand = False
return label, expand
def _apply_expansion_to_tree(self, node: DataTreeNode, search_term: str):
if node.ui_created and not node.is_leaf and node.ui_tag and dpg.does_item_exist(node.ui_tag):
label, expand = self._get_node_label_and_expand(node, search_term)
if expand:
self._expanded_tags.add(node.ui_tag)
dpg.set_value(node.ui_tag, expand)
elif node.ui_tag in self._expanded_tags: # not expanded and was expanded
self._expanded_tags.remove(node.ui_tag)
dpg.set_value(node.ui_tag, expand)
dpg.delete_item(node.ui_tag, children_only=True) # delete children (not visible since collapsed)
self._reset_ui_state_recursive(node)
node.children_ui_created = False
dpg.set_item_label(node.ui_tag, label)
for child in node.created_children.values():
self._apply_expansion_to_tree(child, search_term)
def _reset_ui_state_recursive(self, node: DataTreeNode):
for child in node.created_children.values():
if child.ui_tag is not None:
if item_handler_tag := self._item_handlers.get(child.ui_tag):
self._handlers_to_delete.append(item_handler_tag)
dpg.configure_item(item_handler_tag, show=False)
del self._item_handlers[child.ui_tag]
self._reset_ui_state_recursive(child)
child.ui_created = False
child.ui_tag = None
child.children_ui_created = False
self._current_created_paths.remove(child.full_path)
node.created_children.clear()
def search_data(self):
with self._ui_lock:
self._queued_search = dpg.get_value("search_input")
def _create_tree_node_ui(self, node: DataTreeNode, parent_tag: str, before: str | int):
node.ui_tag = f"tree_{node.full_path}"
search_term = self.current_search.strip().lower()
label, expand = self._get_node_label_and_expand(node, search_term)
if expand:
self._expanded_tags.add(node.ui_tag)
elif node.ui_tag in self._expanded_tags:
self._expanded_tags.remove(node.ui_tag)
with dpg.tree_node(
label=label, parent=parent_tag, tag=node.ui_tag, default_open=expand, open_on_arrow=True, open_on_double_click=True, before=before, delay_search=True
):
with dpg.item_handler_registry() as handler_tag:
dpg.add_item_toggled_open_handler(callback=lambda s, a, u: self._request_children_build(node))
dpg.add_item_visible_handler(callback=lambda s, a, u: self._request_children_build(node))
dpg.bind_item_handler_registry(node.ui_tag, handler_tag)
self._item_handlers[node.ui_tag] = handler_tag
node.ui_created = True
def _create_leaf_ui(self, node: DataTreeNode, parent_tag: str, before: str | int):
node.ui_tag = f"leaf_{node.full_path}"
with dpg.group(parent=parent_tag, tag=node.ui_tag, before=before, delay_search=True):
with dpg.table(header_row=False, policy=dpg.mvTable_SizingStretchProp, delay_search=True):
dpg.add_table_column(init_width_or_weight=0.5)
dpg.add_table_column(init_width_or_weight=0.5)
with dpg.table_row():
dpg.add_text(node.name)
dpg.add_text("N/A", tag=f"value_{node.full_path}")
if node.is_plottable is None:
node.is_plottable = self.data_manager.is_plottable(node.full_path)
if node.is_plottable:
with dpg.drag_payload(parent=node.ui_tag, drag_data=node.full_path, payload_type="TIMESERIES_PAYLOAD"):
dpg.add_text(f"Plot: {node.full_path}")
with dpg.item_handler_registry() as handler_tag:
dpg.add_item_visible_handler(callback=self._on_item_visible, user_data=node.full_path)
dpg.bind_item_handler_registry(node.ui_tag, handler_tag)
self._item_handlers[node.ui_tag] = handler_tag
node.ui_created = True
def _on_item_visible(self, sender, app_data, user_data):
with self._ui_lock:
path = user_data
value_tag = f"value_{path}"
if not dpg.does_item_exist(value_tag):
return
value_column_width = dpg.get_item_rect_size(f"leaf_{path}")[0] // 2
value = self.data_manager.get_value_at(path, self.playback_manager.current_time_s)
if value is not None:
formatted_value = self.format_and_truncate(value, value_column_width, self._char_width)
dpg.set_value(value_tag, formatted_value)
else:
dpg.set_value(value_tag, "N/A")
def _request_children_build(self, node: DataTreeNode):
with self._ui_lock:
if not node.children_ui_created and (node.name == "root" or (node.ui_tag is not None and dpg.get_value(node.ui_tag))): # check root or node expanded
sorted_children = sorted(node.filtered_children.values(), key=self._natural_sort_key)
next_existing: list[int | str] = [0] * len(sorted_children)
current_before_tag: int | str = 0
for i in range(len(sorted_children) - 1, -1, -1): # calculate "before_tag" for correct ordering when incrementally building tree
child = sorted_children[i]
next_existing[i] = current_before_tag
if child.ui_created:
candidate_tag = f"leaf_{child.full_path}" if child.is_leaf else f"tree_{child.full_path}"
if dpg.does_item_exist(candidate_tag):
current_before_tag = candidate_tag
for i, child_node in enumerate(sorted_children):
if not child_node.ui_created:
before_tag = next_existing[i]
self._build_queue[child_node.full_path] = (child_node, node, before_tag)
node.children_ui_created = True
def _should_show_path(self, path: str, search_term: str) -> bool:
if 'DEPRECATED' in path and not os.environ.get('SHOW_DEPRECATED'):
return False
return not search_term or search_term in path.lower()
def _natural_sort_key(self, node: DataTreeNode):
node_type_key = node.is_leaf
parts = [int(p) if p.isdigit() else p.lower() for p in re.split(r'(\d+)', node.name) if p]
return (node_type_key, parts)
def _get_descendant_paths(self, node: DataTreeNode):
for child_name, child_node in node.filtered_children.items():
child_name_lower = child_name.lower()
if child_node.is_leaf:
yield child_name_lower
else:
for path in self._get_descendant_paths(child_node):
yield f"{child_name_lower}/{path}"
@staticmethod
def format_and_truncate(value, available_width: float, char_width: float) -> str:
s = f"{value:.5f}" if np.issubdtype(type(value), np.floating) else str(value)
max_chars = int(available_width / char_width)
if len(s) > max_chars:
return s[: max(0, max_chars - 3)] + "..."
return s
-477
View File
@@ -1,477 +0,0 @@
import dearpygui.dearpygui as dpg
from openpilot.tools.jotpluggler.data import DataManager
from openpilot.tools.jotpluggler.views import TimeSeriesPanel
GRIP_SIZE = 4
MIN_PANE_SIZE = 60
class LayoutManager:
def __init__(self, data_manager, playback_manager, worker_manager, scale: float = 1.0):
self.data_manager = data_manager
self.playback_manager = playback_manager
self.worker_manager = worker_manager
self.scale = scale
self.container_tag = "plot_layout_container"
self.tab_bar_tag = "tab_bar_container"
self.tab_content_tag = "tab_content_area"
self.active_tab = 0
initial_panel_layout = PanelLayoutManager(data_manager, playback_manager, worker_manager, scale)
self.tabs: dict = {0: {"name": "Tab 1", "panel_layout": initial_panel_layout}}
self._next_tab_id = self.active_tab + 1
def to_dict(self) -> dict:
return {
"tabs": {
str(tab_id): {
"name": tab_data["name"],
"panel_layout": tab_data["panel_layout"].to_dict()
}
for tab_id, tab_data in self.tabs.items()
}
}
def clear_and_load_from_dict(self, data: dict):
tab_ids_to_close = list(self.tabs.keys())
for tab_id in tab_ids_to_close:
self.close_tab(tab_id, force=True)
for tab_id_str, tab_data in data["tabs"].items():
tab_id = int(tab_id_str)
panel_layout = PanelLayoutManager.load_from_dict(
tab_data["panel_layout"], self.data_manager, self.playback_manager,
self.worker_manager, self.scale
)
self.tabs[tab_id] = {
"name": tab_data["name"],
"panel_layout": panel_layout
}
self.active_tab = min(self.tabs.keys()) if self.tabs else 0
self._next_tab_id = max(self.tabs.keys()) + 1 if self.tabs else 1
def create_ui(self, parent_tag: str):
if dpg.does_item_exist(self.container_tag):
dpg.delete_item(self.container_tag)
with dpg.child_window(tag=self.container_tag, parent=parent_tag, border=False, width=-1, height=-1, no_scrollbar=True, no_scroll_with_mouse=True):
self._create_tab_bar()
self._create_tab_content()
dpg.bind_item_theme(self.tab_bar_tag, "tab_bar_theme")
def _create_tab_bar(self):
text_size = int(13 * self.scale)
with dpg.child_window(tag=self.tab_bar_tag, parent=self.container_tag, height=(text_size + 8), border=False, horizontal_scrollbar=True):
with dpg.group(horizontal=True, tag="tab_bar_group"):
for tab_id, tab_data in self.tabs.items():
self._create_tab_ui(tab_id, tab_data["name"])
dpg.add_image_button(texture_tag="plus_texture", callback=self.add_tab, width=text_size, height=text_size, tag="add_tab_button")
dpg.bind_item_theme("add_tab_button", "inactive_tab_theme")
def _create_tab_ui(self, tab_id: int, tab_name: str):
text_size = int(13 * self.scale)
tab_width = int(140 * self.scale)
with dpg.child_window(width=tab_width, height=-1, border=False, no_scrollbar=True, tag=f"tab_window_{tab_id}", parent="tab_bar_group"):
with dpg.group(horizontal=True, tag=f"tab_group_{tab_id}"):
dpg.add_input_text(
default_value=tab_name, width=tab_width - text_size - 16, callback=lambda s, v, u: self.rename_tab(u, v), user_data=tab_id, tag=f"tab_input_{tab_id}"
)
dpg.add_image_button(
texture_tag="x_texture", callback=lambda s, a, u: self.close_tab(u), user_data=tab_id, width=text_size, height=text_size, tag=f"tab_close_{tab_id}"
)
with dpg.item_handler_registry(tag=f"tab_handler_{tab_id}"):
dpg.add_item_clicked_handler(callback=lambda s, a, u: self.switch_tab(u), user_data=tab_id)
dpg.bind_item_handler_registry(f"tab_group_{tab_id}", f"tab_handler_{tab_id}")
theme_tag = "active_tab_theme" if tab_id == self.active_tab else "inactive_tab_theme"
dpg.bind_item_theme(f"tab_window_{tab_id}", theme_tag)
def _create_tab_content(self):
with dpg.child_window(tag=self.tab_content_tag, parent=self.container_tag, border=False, width=-1, height=-1, no_scrollbar=True, no_scroll_with_mouse=True):
if self.active_tab in self.tabs:
active_panel_layout = self.tabs[self.active_tab]["panel_layout"]
active_panel_layout.create_ui()
def add_tab(self):
new_panel_layout = PanelLayoutManager(self.data_manager, self.playback_manager, self.worker_manager, self.scale)
new_tab = {"name": f"Tab {self._next_tab_id + 1}", "panel_layout": new_panel_layout}
self.tabs[self._next_tab_id] = new_tab
self._create_tab_ui(self._next_tab_id, new_tab["name"])
dpg.move_item("add_tab_button", parent="tab_bar_group") # move plus button to end
self.switch_tab(self._next_tab_id)
self._next_tab_id += 1
def close_tab(self, tab_id: int, force = False):
if len(self.tabs) <= 1 and not force:
return # don't allow closing the last tab
tab_to_close = self.tabs[tab_id]
tab_to_close["panel_layout"].destroy_ui()
for suffix in ["window", "group", "input", "close", "handler"]:
tag = f"tab_{suffix}_{tab_id}"
if dpg.does_item_exist(tag):
dpg.delete_item(tag)
del self.tabs[tab_id]
if self.active_tab == tab_id and self.tabs: # switch to another tab if we closed the active one
self.active_tab = next(iter(self.tabs.keys()))
self._switch_tab_content()
dpg.bind_item_theme(f"tab_window_{self.active_tab}", "active_tab_theme")
def switch_tab(self, tab_id: int):
if tab_id == self.active_tab or tab_id not in self.tabs:
return
current_panel_layout = self.tabs[self.active_tab]["panel_layout"]
current_panel_layout.destroy_ui()
dpg.bind_item_theme(f"tab_window_{self.active_tab}", "inactive_tab_theme") # deactivate old tab
self.active_tab = tab_id
dpg.bind_item_theme(f"tab_window_{tab_id}", "active_tab_theme") # activate new tab
self._switch_tab_content()
def _switch_tab_content(self):
dpg.delete_item(self.tab_content_tag, children_only=True)
active_panel_layout = self.tabs[self.active_tab]["panel_layout"]
active_panel_layout.create_ui()
active_panel_layout.update_all_panels()
def rename_tab(self, tab_id: int, new_name: str):
if tab_id in self.tabs:
self.tabs[tab_id]["name"] = new_name
def update_all_panels(self):
self.tabs[self.active_tab]["panel_layout"].update_all_panels()
def on_viewport_resize(self):
self.tabs[self.active_tab]["panel_layout"].on_viewport_resize()
class PanelLayoutManager:
def __init__(self, data_manager: DataManager, playback_manager, worker_manager, scale: float = 1.0):
self.data_manager = data_manager
self.playback_manager = playback_manager
self.worker_manager = worker_manager
self.scale = scale
self.active_panels: list = []
self.parent_tag = "tab_content_area"
self._queue_resize = False
self._created_handler_tags: set[str] = set()
self.grip_size = int(GRIP_SIZE * self.scale)
self.min_pane_size = int(MIN_PANE_SIZE * self.scale)
initial_panel = TimeSeriesPanel(data_manager, playback_manager, worker_manager)
self.layout: dict = {"type": "panel", "panel": initial_panel}
def to_dict(self) -> dict:
return self._layout_to_dict(self.layout)
def _layout_to_dict(self, layout: dict) -> dict:
if layout["type"] == "panel":
return {
"type": "panel",
"panel": layout["panel"].to_dict()
}
else: # split
return {
"type": "split",
"orientation": layout["orientation"],
"proportions": layout["proportions"],
"children": [self._layout_to_dict(child) for child in layout["children"]]
}
@classmethod
def load_from_dict(cls, data: dict, data_manager, playback_manager, worker_manager, scale: float = 1.0):
manager = cls(data_manager, playback_manager, worker_manager, scale)
manager.layout = manager._dict_to_layout(data)
return manager
def _dict_to_layout(self, data: dict) -> dict:
if data["type"] == "panel":
panel_data = data["panel"]
if panel_data["type"] == "timeseries":
panel = TimeSeriesPanel.load_from_dict(
panel_data, self.data_manager, self.playback_manager, self.worker_manager
)
return {"type": "panel", "panel": panel}
else:
# Handle future panel types here or make a general mapping
raise ValueError(f"Unknown panel type: {panel_data['type']}")
else: # split
return {
"type": "split",
"orientation": data["orientation"],
"proportions": data["proportions"],
"children": [self._dict_to_layout(child) for child in data["children"]]
}
def create_ui(self):
self.active_panels.clear()
if dpg.does_item_exist(self.parent_tag):
dpg.delete_item(self.parent_tag, children_only=True)
self._cleanup_all_handlers()
container_width, container_height = dpg.get_item_rect_size(self.parent_tag)
if container_width == 0 and container_height == 0:
self._queue_resize = True
self._create_ui_recursive(self.layout, self.parent_tag, [], container_width, container_height)
def destroy_ui(self):
self._cleanup_ui_recursive(self.layout, [])
self._cleanup_all_handlers()
self.active_panels.clear()
def _cleanup_all_handlers(self):
for handler_tag in list(self._created_handler_tags):
if dpg.does_item_exist(handler_tag):
dpg.delete_item(handler_tag)
self._created_handler_tags.clear()
def _create_ui_recursive(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int):
if layout["type"] == "panel":
self._create_panel_ui(layout, parent_tag, path, width, height)
else:
self._create_split_ui(layout, parent_tag, path, width, height)
def _create_panel_ui(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int):
panel_tag = self._path_to_tag(path, "panel")
panel = layout["panel"]
self.active_panels.append(panel)
text_size = int(13 * self.scale)
bar_height = (text_size + 24) if width < int(329 * self.scale + 64) else (text_size + 8) # adjust height to allow for scrollbar
with dpg.child_window(parent=parent_tag, border=False, width=-1, height=-1, no_scrollbar=True):
with dpg.group(horizontal=True):
with dpg.child_window(tag=panel_tag, width=-(text_size + 16), height=bar_height, horizontal_scrollbar=True, no_scroll_with_mouse=True, border=False):
with dpg.group(horizontal=True):
# if you change the widths make sure to change the sum of widths (currently 329 * scale)
dpg.add_input_text(default_value=panel.title, width=int(150 * self.scale), callback=lambda s, v: setattr(panel, "title", v))
dpg.add_combo(items=["Time Series"], default_value="Time Series", width=int(100 * self.scale))
dpg.add_button(label="Clear", callback=lambda: self.clear_panel(panel), width=int(40 * self.scale))
dpg.add_image_button(texture_tag="split_h_texture", callback=lambda: self.split_panel(path, 0), width=text_size, height=text_size)
dpg.add_image_button(texture_tag="split_v_texture", callback=lambda: self.split_panel(path, 1), width=text_size, height=text_size)
dpg.add_image_button(texture_tag="x_texture", callback=lambda: self.delete_panel(path), width=text_size, height=text_size)
dpg.add_separator()
content_tag = self._path_to_tag(path, "content")
with dpg.child_window(tag=content_tag, border=False, height=-1, width=-1, no_scrollbar=True):
panel.create_ui(content_tag)
def _create_split_ui(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int):
split_tag = self._path_to_tag(path, "split")
orientation, _, pane_sizes = self._get_split_geometry(layout, (width, height))
with dpg.group(tag=split_tag, parent=parent_tag, horizontal=orientation == 0):
for i, child_layout in enumerate(layout["children"]):
child_path = path + [i]
container_tag = self._path_to_tag(child_path, "container")
pane_width, pane_height = [(pane_sizes[i], -1), (-1, pane_sizes[i])][orientation] # fill 2nd dim up to the border
with dpg.child_window(tag=container_tag, width=pane_width, height=pane_height, border=False, no_scrollbar=True):
child_width, child_height = [(pane_sizes[i], height), (width, pane_sizes[i])][orientation]
self._create_ui_recursive(child_layout, container_tag, child_path, child_width, child_height)
if i < len(layout["children"]) - 1:
self._create_grip(split_tag, path, i, orientation)
def clear_panel(self, panel):
panel.clear()
def delete_panel(self, panel_path: list[int]):
if not panel_path: # Root deletion
old_panel = self.layout["panel"]
old_panel.destroy_ui()
self.active_panels.remove(old_panel)
new_panel = TimeSeriesPanel(self.data_manager, self.playback_manager, self.worker_manager)
self.layout = {"type": "panel", "panel": new_panel}
self._rebuild_ui_at_path([])
return
parent, child_index = self._get_parent_and_index(panel_path)
layout_to_delete = parent["children"][child_index]
self._cleanup_ui_recursive(layout_to_delete, panel_path)
parent["children"].pop(child_index)
parent["proportions"].pop(child_index)
if len(parent["children"]) == 1: # remove parent and collapse
remaining_child = parent["children"][0]
if len(panel_path) == 1: # parent is at root level - promote remaining child to root
self.layout = remaining_child
self._rebuild_ui_at_path([])
else: # replace parent with remaining child in grandparent
grandparent_path = panel_path[:-2]
parent_index = panel_path[-2]
self._replace_layout_at_path(grandparent_path + [parent_index], remaining_child)
self._rebuild_ui_at_path(grandparent_path + [parent_index])
else: # redistribute proportions
equal_prop = 1.0 / len(parent["children"])
parent["proportions"] = [equal_prop] * len(parent["children"])
self._rebuild_ui_at_path(panel_path[:-1])
def split_panel(self, panel_path: list[int], orientation: int):
current_layout = self._get_layout_at_path(panel_path)
existing_panel = current_layout["panel"]
new_panel = TimeSeriesPanel(self.data_manager, self.playback_manager, self.worker_manager)
parent, child_index = self._get_parent_and_index(panel_path)
if parent is None: # Root split
self.layout = {
"type": "split",
"orientation": orientation,
"children": [{"type": "panel", "panel": existing_panel}, {"type": "panel", "panel": new_panel}],
"proportions": [0.5, 0.5],
}
self._rebuild_ui_at_path([])
elif parent["type"] == "split" and parent["orientation"] == orientation: # Same orientation - insert into existing split
parent["children"].insert(child_index + 1, {"type": "panel", "panel": new_panel})
parent["proportions"] = [1.0 / len(parent["children"])] * len(parent["children"])
self._rebuild_ui_at_path(panel_path[:-1])
else: # Different orientation - create new split level
new_split = {"type": "split", "orientation": orientation, "children": [current_layout, {"type": "panel", "panel": new_panel}], "proportions": [0.5, 0.5]}
self._replace_layout_at_path(panel_path, new_split)
self._rebuild_ui_at_path(panel_path)
def _rebuild_ui_at_path(self, path: list[int]):
layout = self._get_layout_at_path(path)
if path:
container_tag = self._path_to_tag(path, "container")
else: # Root update
container_tag = self.parent_tag
self._cleanup_ui_recursive(layout, path)
dpg.delete_item(container_tag, children_only=True)
width, height = dpg.get_item_rect_size(container_tag)
self._create_ui_recursive(layout, container_tag, path, width, height)
def _cleanup_ui_recursive(self, layout: dict, path: list[int]):
if layout["type"] == "panel":
panel = layout["panel"]
panel.destroy_ui()
if panel in self.active_panels:
self.active_panels.remove(panel)
else:
for i in range(len(layout["children"]) - 1):
handler_tag = f"{self._path_to_tag(path, f'grip_{i}')}_handler"
if dpg.does_item_exist(handler_tag):
dpg.delete_item(handler_tag)
self._created_handler_tags.discard(handler_tag)
for i, child in enumerate(layout["children"]):
self._cleanup_ui_recursive(child, path + [i])
def update_all_panels(self):
if self._queue_resize:
if (size := dpg.get_item_rect_size(self.parent_tag)) != [0, 0]:
self._queue_resize = False
self._resize_splits_recursive(self.layout, [], *size)
for panel in self.active_panels:
panel.update()
def on_viewport_resize(self):
self._resize_splits_recursive(self.layout, [])
def _resize_splits_recursive(self, layout: dict, path: list[int], width: int | None = None, height: int | None = None):
if layout["type"] == "split":
split_tag = self._path_to_tag(path, "split")
if dpg.does_item_exist(split_tag):
available_sizes = (width, height) if width and height else dpg.get_item_rect_size(dpg.get_item_parent(split_tag))
orientation, _, pane_sizes = self._get_split_geometry(layout, available_sizes)
size_properties = ("width", "height")
for i, child_layout in enumerate(layout["children"]):
child_path = path + [i]
container_tag = self._path_to_tag(child_path, "container")
if dpg.does_item_exist(container_tag):
dpg.configure_item(container_tag, **{size_properties[orientation]: pane_sizes[i]})
child_width, child_height = [(pane_sizes[i], available_sizes[1]), (available_sizes[0], pane_sizes[i])][orientation]
self._resize_splits_recursive(child_layout, child_path, child_width, child_height)
else: # leaf node/panel - adjust bar height to allow for scrollbar
panel_tag = self._path_to_tag(path, "panel")
if width is not None and width < int(329 * self.scale + 64): # scaled widths of the elements in top bar + fixed 8 padding on left and right of each item
dpg.configure_item(panel_tag, height=(int(13 * self.scale) + 24))
else:
dpg.configure_item(panel_tag, height=(int(13 * self.scale) + 8))
def _get_split_geometry(self, layout: dict, available_size: tuple[int, int]) -> tuple[int, int, list[int]]:
orientation = layout["orientation"]
num_grips = len(layout["children"]) - 1
usable_size = max(self.min_pane_size, available_size[orientation] - (num_grips * (self.grip_size + 8 * (2 - orientation)))) # approximate, scaling is weird
pane_sizes = [max(self.min_pane_size, int(usable_size * prop)) for prop in layout["proportions"]]
return orientation, usable_size, pane_sizes
def _get_layout_at_path(self, path: list[int]) -> dict:
current = self.layout
for index in path:
current = current["children"][index]
return current
def _get_parent_and_index(self, path: list[int]) -> tuple:
return (None, -1) if not path else (self._get_layout_at_path(path[:-1]), path[-1])
def _replace_layout_at_path(self, path: list[int], new_layout: dict):
if not path:
self.layout = new_layout
else:
parent, index = self._get_parent_and_index(path)
parent["children"][index] = new_layout
def _path_to_tag(self, path: list[int], prefix: str = "") -> str:
path_str = "_".join(map(str, path)) if path else "root"
return f"{prefix}_{path_str}" if prefix else path_str
def _create_grip(self, parent_tag: str, path: list[int], grip_index: int, orientation: int):
grip_tag = self._path_to_tag(path, f"grip_{grip_index}")
handler_tag = f"{grip_tag}_handler"
width, height = [(self.grip_size, -1), (-1, self.grip_size)][orientation]
with dpg.child_window(tag=grip_tag, parent=parent_tag, width=width, height=height, no_scrollbar=True, border=False):
button_tag = dpg.add_button(label="", width=-1, height=-1)
with dpg.item_handler_registry(tag=handler_tag):
user_data = (path, grip_index, orientation)
dpg.add_item_active_handler(callback=self._on_grip_drag, user_data=user_data)
dpg.add_item_deactivated_handler(callback=self._on_grip_end, user_data=user_data)
dpg.bind_item_handler_registry(button_tag, handler_tag)
self._created_handler_tags.add(handler_tag)
def _on_grip_drag(self, sender, app_data, user_data):
path, grip_index, orientation = user_data
layout = self._get_layout_at_path(path)
if "_drag_data" not in layout:
layout["_drag_data"] = {"initial_proportions": layout["proportions"][:], "start_mouse": dpg.get_mouse_pos(local=False)[orientation]}
return
drag_data = layout["_drag_data"]
split_tag = self._path_to_tag(path, "split")
if not dpg.does_item_exist(split_tag):
return
_, usable_size, _ = self._get_split_geometry(layout, dpg.get_item_rect_size(split_tag))
current_coord = dpg.get_mouse_pos(local=False)[orientation]
delta = current_coord - drag_data["start_mouse"]
delta_prop = delta / usable_size
left_idx = grip_index
right_idx = left_idx + 1
initial = drag_data["initial_proportions"]
min_prop = self.min_pane_size / usable_size
new_left = max(min_prop, initial[left_idx] + delta_prop)
new_right = max(min_prop, initial[right_idx] - delta_prop)
total_available = initial[left_idx] + initial[right_idx]
if new_left + new_right > total_available:
if new_left > new_right:
new_left = total_available - new_right
else:
new_right = total_available - new_left
layout["proportions"] = initial[:]
layout["proportions"][left_idx] = new_left
layout["proportions"][right_idx] = new_right
self._resize_splits_recursive(layout, path)
def _on_grip_end(self, sender, app_data, user_data):
path, _, _ = user_data
self._get_layout_at_path(path).pop("_drag_data", None)
@@ -1,128 +0,0 @@
tabs:
'0':
name: Lateral Plan Conformance
panel_layout:
type: split
orientation: 1
proportions:
- 0.3333333333333333
- 0.3333333333333333
- 0.3333333333333333
children:
- type: panel
panel:
type: timeseries
title: desired vs actual
series_paths:
- controlsState/lateralControlState/torqueState/desiredLateralAccel
- controlsState/lateralControlState/torqueState/actualLateralAccel
- type: panel
panel:
type: timeseries
title: ff vs output
series_paths:
- controlsState/lateralControlState/torqueState/f
- carState/steeringPressed
- carControl/actuators/torque
- type: panel
panel:
type: timeseries
title: vehicle speed
series_paths:
- carState/vEgo
'1':
name: Actuator Performance
panel_layout:
type: split
orientation: 1
proportions:
- 0.3333333333333333
- 0.3333333333333333
- 0.3333333333333333
children:
- type: panel
panel:
type: timeseries
title: calc vs learned latAccelFactor
series_paths:
- liveTorqueParameters/latAccelFactorFiltered
- liveTorqueParameters/latAccelFactorRaw
- carParams/lateralTuning/torque/latAccelFactor
- type: panel
panel:
type: timeseries
title: learned latAccelOffset
series_paths:
- liveTorqueParameters/latAccelOffsetRaw
- liveTorqueParameters/latAccelOffsetFiltered
- type: panel
panel:
type: timeseries
title: calc vs learned friction
series_paths:
- liveTorqueParameters/frictionCoefficientFiltered
- liveTorqueParameters/frictionCoefficientRaw
- carParams/lateralTuning/torque/friction
'2':
name: Vehicle Dynamics
panel_layout:
type: split
orientation: 1
proportions:
- 0.3333333333333333
- 0.3333333333333333
- 0.3333333333333333
children:
- type: panel
panel:
type: timeseries
title: initial vs learned steerRatio
series_paths:
- carParams/steerRatio
- liveParameters/steerRatio
- type: panel
panel:
type: timeseries
title: initial vs learned tireStiffnessFactor
series_paths:
- carParams/tireStiffnessFactor
- liveParameters/stiffnessFactor
- type: panel
panel:
type: timeseries
title: live steering angle offsets
series_paths:
- liveParameters/angleOffsetDeg
- liveParameters/angleOffsetAverageDeg
'3':
name: Controller PIF Terms
panel_layout:
type: split
orientation: 1
proportions:
- 0.3333333333333333
- 0.3333333333333333
- 0.3333333333333333
children:
- type: panel
panel:
type: timeseries
title: ff vs output
series_paths:
- carControl/actuators/torque
- controlsState/lateralControlState/torqueState/f
- carState/steeringPressed
- type: panel
panel:
type: timeseries
title: PIF terms
series_paths:
- controlsState/lateralControlState/torqueState/f
- controlsState/lateralControlState/torqueState/p
- controlsState/lateralControlState/torqueState/i
- type: panel
panel:
type: timeseries
title: road roll angle
series_paths:
- liveParameters/roll
-368
View File
@@ -1,368 +0,0 @@
#!/usr/bin/env python3
import argparse
import os
import dearpygui.dearpygui as dpg
import multiprocessing
import uuid
import signal
import yaml
from openpilot.common.swaglog import cloudlog
from openpilot.common.basedir import BASEDIR
from openpilot.tools.jotpluggler.data import DataManager
from openpilot.tools.jotpluggler.datatree import DataTree
from openpilot.tools.jotpluggler.layout import LayoutManager
DEMO_ROUTE = "5beb9b58bd12b691/0000010a--a51155e496"
class WorkerManager:
def __init__(self, max_workers=None):
self.pool = multiprocessing.Pool(max_workers or min(4, multiprocessing.cpu_count()), initializer=WorkerManager.worker_initializer)
self.active_tasks = {}
def submit_task(self, func, args_list, callback=None, task_id=None):
task_id = task_id or str(uuid.uuid4())
if task_id in self.active_tasks:
try:
self.active_tasks[task_id].terminate()
except Exception:
pass
def handle_success(result):
self.active_tasks.pop(task_id, None)
if callback:
try:
callback(result)
except Exception as e:
print(f"Callback for task {task_id} failed: {e}")
def handle_error(error):
self.active_tasks.pop(task_id, None)
print(f"Task {task_id} failed: {error}")
async_result = self.pool.starmap_async(func, args_list, callback=handle_success, error_callback=handle_error)
self.active_tasks[task_id] = async_result
return task_id
@staticmethod
def worker_initializer():
signal.signal(signal.SIGINT, signal.SIG_IGN)
def shutdown(self):
for task in self.active_tasks.values():
try:
task.terminate()
except Exception:
pass
self.pool.terminate()
self.pool.join()
class PlaybackManager:
def __init__(self):
self.is_playing = False
self.current_time_s = 0.0
self.duration_s = 0.0
self.num_segments = 0
self.x_axis_bounds = (0.0, 0.0) # (min_time, max_time)
self.x_axis_observers = [] # callbacks for x-axis changes
self._updating_x_axis = False
def set_route_duration(self, duration: float):
self.duration_s = duration
self.seek(min(self.current_time_s, duration))
def toggle_play_pause(self):
if not self.is_playing and self.current_time_s >= self.duration_s:
self.seek(0.0)
self.is_playing = not self.is_playing
texture_tag = "pause_texture" if self.is_playing else "play_texture"
dpg.configure_item("play_pause_button", texture_tag=texture_tag)
def seek(self, time_s: float):
self.current_time_s = max(0.0, min(time_s, self.duration_s))
def update_time(self, delta_t: float):
if self.is_playing:
self.current_time_s = min(self.current_time_s + delta_t, self.duration_s)
if self.current_time_s >= self.duration_s:
self.is_playing = False
dpg.configure_item("play_pause_button", texture_tag="play_texture")
return self.current_time_s
def set_x_axis_bounds(self, min_time: float, max_time: float, source_panel=None):
if self._updating_x_axis:
return
new_bounds = (min_time, max_time)
if new_bounds == self.x_axis_bounds:
return
self.x_axis_bounds = new_bounds
self._updating_x_axis = True # prevent recursive updates
try:
for callback in self.x_axis_observers:
try:
callback(min_time, max_time, source_panel)
except Exception as e:
print(f"Error in x-axis sync callback: {e}")
finally:
self._updating_x_axis = False
def add_x_axis_observer(self, callback):
if callback not in self.x_axis_observers:
self.x_axis_observers.append(callback)
def remove_x_axis_observer(self, callback):
if callback in self.x_axis_observers:
self.x_axis_observers.remove(callback)
class MainController:
def __init__(self, scale: float = 1.0):
self.scale = scale
self.data_manager = DataManager()
self.playback_manager = PlaybackManager()
self.worker_manager = WorkerManager()
self._create_global_themes()
self.data_tree = DataTree(self.data_manager, self.playback_manager)
self.layout_manager = LayoutManager(self.data_manager, self.playback_manager, self.worker_manager, scale=self.scale)
self.data_manager.add_observer(self.on_data_loaded)
self._total_segments = 0
def _create_global_themes(self):
with dpg.theme(tag="line_theme"):
with dpg.theme_component(dpg.mvLineSeries):
scaled_thickness = max(1.0, self.scale)
dpg.add_theme_style(dpg.mvPlotStyleVar_LineWeight, scaled_thickness, category=dpg.mvThemeCat_Plots)
with dpg.theme(tag="timeline_theme"):
with dpg.theme_component(dpg.mvInfLineSeries):
scaled_thickness = max(1.0, self.scale)
dpg.add_theme_style(dpg.mvPlotStyleVar_LineWeight, scaled_thickness, category=dpg.mvThemeCat_Plots)
dpg.add_theme_color(dpg.mvPlotCol_Line, (255, 0, 0, 128), category=dpg.mvThemeCat_Plots)
for tag, color in (("active_tab_theme", (37, 37, 38, 255)), ("inactive_tab_theme", (70, 70, 75, 255))):
with dpg.theme(tag=tag):
for cmp, target in ((dpg.mvChildWindow, dpg.mvThemeCol_ChildBg), (dpg.mvInputText, dpg.mvThemeCol_FrameBg), (dpg.mvImageButton, dpg.mvThemeCol_Button)):
with dpg.theme_component(cmp):
dpg.add_theme_color(target, color)
with dpg.theme(tag="tab_bar_theme"):
with dpg.theme_component(dpg.mvChildWindow):
dpg.add_theme_color(dpg.mvThemeCol_ChildBg, (51, 51, 55, 255))
def on_data_loaded(self, data: dict):
duration = data.get('duration', 0.0)
self.playback_manager.set_route_duration(duration)
if data.get('metadata_loaded'):
self.playback_manager.num_segments = data.get('total_segments', 0)
self._total_segments = data.get('total_segments', 0)
dpg.set_value("load_status", f"Loading... 0/{self._total_segments} segments processed")
elif data.get('reset'):
self.playback_manager.current_time_s = 0.0
self.playback_manager.duration_s = 0.0
self.playback_manager.is_playing = False
self._total_segments = 0
dpg.set_value("load_status", "Loading...")
dpg.set_value("timeline_slider", 0.0)
dpg.configure_item("timeline_slider", max_value=0.0)
dpg.configure_item("play_pause_button", texture_tag="play_texture")
dpg.configure_item("load_button", enabled=True)
elif data.get('loading_complete'):
num_paths = len(self.data_manager.get_all_paths())
dpg.set_value("load_status", f"Loaded {num_paths} data paths")
dpg.configure_item("load_button", enabled=True)
elif data.get('segment_added'):
segment_count = data.get('segment_count', 0)
dpg.set_value("load_status", f"Loading... {segment_count}/{self._total_segments} segments processed")
dpg.configure_item("timeline_slider", max_value=duration)
def save_layout_to_yaml(self, filepath: str):
layout_dict = self.layout_manager.to_dict()
with open(filepath, 'w') as f:
yaml.dump(layout_dict, f, default_flow_style=False, sort_keys=False)
def load_layout_from_yaml(self, filepath: str):
with open(filepath) as f:
layout_dict = yaml.safe_load(f)
self.layout_manager.clear_and_load_from_dict(layout_dict)
self.layout_manager.create_ui("main_plot_area")
def save_layout_dialog(self):
if dpg.does_item_exist("save_layout_dialog"):
dpg.delete_item("save_layout_dialog")
with dpg.file_dialog(
callback=self._save_layout_callback, tag="save_layout_dialog", width=int(700 * self.scale), height=int(400 * self.scale),
default_filename="layout", default_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), "layouts")
):
dpg.add_file_extension(".yaml")
def load_layout_dialog(self):
if dpg.does_item_exist("load_layout_dialog"):
dpg.delete_item("load_layout_dialog")
with dpg.file_dialog(
callback=self._load_layout_callback, tag="load_layout_dialog", width=int(700 * self.scale), height=int(400 * self.scale),
default_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), "layouts")
):
dpg.add_file_extension(".yaml")
def _save_layout_callback(self, sender, app_data):
filepath = app_data['file_path_name']
try:
self.save_layout_to_yaml(filepath)
dpg.set_value("load_status", f"Layout saved to {os.path.basename(filepath)}")
except Exception:
dpg.set_value("load_status", "Error saving layout")
cloudlog.exception(f"Error saving layout to {filepath}")
dpg.delete_item("save_layout_dialog")
def _load_layout_callback(self, sender, app_data):
filepath = app_data['file_path_name']
try:
self.load_layout_from_yaml(filepath)
dpg.set_value("load_status", f"Layout loaded from {os.path.basename(filepath)}")
except Exception:
dpg.set_value("load_status", "Error loading layout")
cloudlog.exception(f"Error loading layout from {filepath}:")
dpg.delete_item("load_layout_dialog")
def setup_ui(self):
with dpg.texture_registry():
script_dir = os.path.dirname(os.path.realpath(__file__))
for image in ["play", "pause", "x", "split_h", "split_v", "plus"]:
texture = dpg.load_image(os.path.join(script_dir, "assets", f"{image}.png"))
dpg.add_static_texture(width=texture[0], height=texture[1], default_value=texture[3], tag=f"{image}_texture")
with dpg.window(tag="Primary Window"):
with dpg.group(horizontal=True):
# Left panel - Data tree
with dpg.child_window(label="Sidebar", width=int(300 * self.scale), tag="sidebar_window", border=True, resizable_x=True):
with dpg.group(horizontal=True):
dpg.add_input_text(tag="route_input", width=int(-75 * self.scale), hint="Enter route name...")
dpg.add_button(label="Load", callback=self.load_route, tag="load_button", width=-1)
dpg.add_text("Ready to load route", tag="load_status")
dpg.add_separator()
with dpg.table(header_row=False, policy=dpg.mvTable_SizingStretchProp):
dpg.add_table_column(init_width_or_weight=0.5)
dpg.add_table_column(init_width_or_weight=0.5)
with dpg.table_row():
dpg.add_button(label="Save Layout", callback=self.save_layout_dialog, width=-1)
dpg.add_button(label="Load Layout", callback=self.load_layout_dialog, width=-1)
dpg.add_separator()
self.data_tree.create_ui("sidebar_window")
# Right panel - Plots and timeline
with dpg.group(tag="right_panel"):
with dpg.child_window(label="Plot Window", border=True, height=int(-(32 + 13 * self.scale)), tag="main_plot_area"):
self.layout_manager.create_ui("main_plot_area")
with dpg.child_window(label="Timeline", border=True):
with dpg.table(header_row=False):
btn_size = int(13 * self.scale)
dpg.add_table_column(width_fixed=True, init_width_or_weight=(btn_size + 8)) # Play button
dpg.add_table_column(width_stretch=True) # Timeline slider
dpg.add_table_column(width_fixed=True, init_width_or_weight=int(50 * self.scale)) # FPS counter
with dpg.table_row():
dpg.add_image_button(texture_tag="play_texture", tag="play_pause_button", callback=self.toggle_play_pause, width=btn_size, height=btn_size)
dpg.add_slider_float(tag="timeline_slider", default_value=0.0, label="", width=-1, callback=self.timeline_drag)
dpg.add_text("", tag="fps_counter")
with dpg.item_handler_registry(tag="plot_resize_handler"):
dpg.add_item_resize_handler(callback=self.on_plot_resize)
dpg.bind_item_handler_registry("right_panel", "plot_resize_handler")
dpg.set_primary_window("Primary Window", True)
def on_plot_resize(self, sender, app_data, user_data):
self.layout_manager.on_viewport_resize()
def load_route(self):
route_name = dpg.get_value("route_input").strip()
if route_name:
dpg.set_value("load_status", "Loading route...")
dpg.configure_item("load_button", enabled=False)
self.data_manager.load_route(route_name)
def toggle_play_pause(self, sender):
self.playback_manager.toggle_play_pause()
def timeline_drag(self, sender, app_data):
self.playback_manager.seek(app_data)
def update_frame(self, font):
self.data_tree.update_frame(font)
new_time = self.playback_manager.update_time(dpg.get_delta_time())
if not dpg.is_item_active("timeline_slider"):
dpg.set_value("timeline_slider", new_time)
self.layout_manager.update_all_panels()
dpg.set_value("fps_counter", f"{dpg.get_frame_rate():.1f} FPS")
def shutdown(self):
self.worker_manager.shutdown()
def main(route_to_load=None, layout_to_load=None):
dpg.create_context()
# TODO: find better way of calculating display scaling
#try:
# w, h = next(tuple(map(int, l.split()[0].split('x'))) for l in subprocess.check_output(['xrandr']).decode().split('\n') if '*' in l) # actual resolution
# scale = pyautogui.size()[0] / w # scaled resolution
#except Exception:
# scale = 1
scale = 1
with dpg.font_registry():
default_font = dpg.add_font(os.path.join(BASEDIR, "selfdrive/assets/fonts/JetBrainsMono-Medium.ttf"), int(13 * scale * 2)) # 2x then scale for hidpi
dpg.bind_font(default_font)
dpg.set_global_font_scale(0.5)
viewport_width, viewport_height = int(1200 * scale), int(800 * scale)
dpg.create_viewport(
title='JotPluggler', width=viewport_width, height=viewport_height,
)
dpg.setup_dearpygui()
controller = MainController(scale=scale)
controller.setup_ui()
if layout_to_load:
try:
controller.load_layout_from_yaml(layout_to_load)
print(f"Loaded layout from {layout_to_load}")
except Exception as e:
print(f"Failed to load layout from {layout_to_load}: {e}")
cloudlog.exception(f"Error loading layout from {layout_to_load}")
if route_to_load:
dpg.set_value("route_input", route_to_load)
controller.load_route()
dpg.show_viewport()
# Main loop
try:
while dpg.is_dearpygui_running():
controller.update_frame(default_font)
dpg.render_dearpygui_frame()
finally:
controller.shutdown()
dpg.destroy_context()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="A tool for visualizing openpilot logs.")
parser.add_argument("--demo", action="store_true", help="Use the demo route instead of providing one")
parser.add_argument("--layout", type=str, help="Path to YAML layout file to load on startup")
parser.add_argument("route", nargs='?', default=None, help="Optional route name to load on startup.")
args = parser.parse_args()
route = DEMO_ROUTE if args.demo else args.route
main(route_to_load=route, layout_to_load=args.layout)
-294
View File
@@ -1,294 +0,0 @@
import uuid
import threading
import numpy as np
from collections import deque
import dearpygui.dearpygui as dpg
from abc import ABC, abstractmethod
class ViewPanel(ABC):
"""Abstract base class for all view panels that can be displayed in a plot container"""
def __init__(self, panel_id: str | None = None):
self.panel_id = panel_id or str(uuid.uuid4())
self.title = "Untitled Panel"
@abstractmethod
def clear(self):
pass
@abstractmethod
def create_ui(self, parent_tag: str):
pass
@abstractmethod
def destroy_ui(self):
pass
@abstractmethod
def get_panel_type(self) -> str:
pass
@abstractmethod
def update(self):
pass
@abstractmethod
def to_dict(self) -> dict:
pass
@classmethod
@abstractmethod
def load_from_dict(cls, data: dict, data_manager, playback_manager, worker_manager):
pass
class TimeSeriesPanel(ViewPanel):
def __init__(self, data_manager, playback_manager, worker_manager, panel_id: str | None = None):
super().__init__(panel_id)
self.data_manager = data_manager
self.playback_manager = playback_manager
self.worker_manager = worker_manager
self.title = "Time Series Plot"
self.plot_tag = f"plot_{self.panel_id}"
self.x_axis_tag = f"{self.plot_tag}_x_axis"
self.y_axis_tag = f"{self.plot_tag}_y_axis"
self.timeline_indicator_tag = f"{self.plot_tag}_timeline"
self._ui_created = False
self._series_data: dict[str, tuple[np.ndarray, np.ndarray]] = {}
self._last_plot_duration = 0
self._update_lock = threading.RLock()
self._results_deque: deque[tuple[str, list, list]] = deque()
self._new_data = False
self._last_x_limits = (0.0, 0.0)
self._queued_x_sync: tuple | None = None
self._queued_reallow_x_zoom = False
self._total_segments = self.playback_manager.num_segments
def to_dict(self) -> dict:
return {
"type": "timeseries",
"title": self.title,
"series_paths": list(self._series_data.keys())
}
@classmethod
def load_from_dict(cls, data: dict, data_manager, playback_manager, worker_manager):
panel = cls(data_manager, playback_manager, worker_manager)
panel.title = data.get("title", "Time Series Plot")
panel._series_data = {path: (np.array([]), np.array([])) for path in data.get("series_paths", [])}
return panel
def create_ui(self, parent_tag: str):
self.data_manager.add_observer(self.on_data_loaded)
self.playback_manager.add_x_axis_observer(self._on_x_axis_sync)
with dpg.plot(height=-1, width=-1, tag=self.plot_tag, parent=parent_tag, drop_callback=self._on_series_drop, payload_type="TIMESERIES_PAYLOAD"):
dpg.add_plot_legend()
dpg.add_plot_axis(dpg.mvXAxis, no_label=True, tag=self.x_axis_tag)
dpg.add_plot_axis(dpg.mvYAxis, no_label=True, tag=self.y_axis_tag)
timeline_series_tag = dpg.add_inf_line_series(x=[0], label="Timeline", parent=self.y_axis_tag, tag=self.timeline_indicator_tag)
dpg.bind_item_theme(timeline_series_tag, "timeline_theme")
self._new_data = True
self._queued_x_sync = self.playback_manager.x_axis_bounds
self._ui_created = True
def update(self):
with self._update_lock:
if not self._ui_created:
return
if self._queued_x_sync:
min_time, max_time = self._queued_x_sync
self._queued_x_sync = None
dpg.set_axis_limits(self.x_axis_tag, min_time, max_time)
self._last_x_limits = (min_time, max_time)
self._fit_y_axis(min_time, max_time)
self._queued_reallow_x_zoom = True # must wait a frame before allowing user changes so that axis limits take effect
return
if self._queued_reallow_x_zoom:
self._queued_reallow_x_zoom = False
if tuple(dpg.get_axis_limits(self.x_axis_tag)) == self._last_x_limits:
dpg.set_axis_limits_auto(self.x_axis_tag)
else:
self._queued_x_sync = self._last_x_limits # retry, likely too early
return
if self._new_data: # handle new data in main thread
self._new_data = False
if self._total_segments > 0:
dpg.set_axis_limits_constraints(self.x_axis_tag, -10, self._total_segments * 60 + 10)
self._fit_y_axis(*dpg.get_axis_limits(self.x_axis_tag))
for series_path in list(self._series_data.keys()):
self.add_series(series_path, update=True)
current_limits = dpg.get_axis_limits(self.x_axis_tag)
# downsample if plot zoom changed significantly
plot_duration = current_limits[1] - current_limits[0]
if plot_duration > self._last_plot_duration * 2 or plot_duration < self._last_plot_duration * 0.5:
self._downsample_all_series(plot_duration)
# sync x-axis if changed by user
if self._last_x_limits != current_limits:
self.playback_manager.set_x_axis_bounds(current_limits[0], current_limits[1], source_panel=self)
self._last_x_limits = current_limits
self._fit_y_axis(current_limits[0], current_limits[1])
while self._results_deque: # handle downsampled results in main thread
results = self._results_deque.popleft()
for series_path, downsampled_time, downsampled_values in results:
series_tag = f"series_{self.panel_id}_{series_path}"
if dpg.does_item_exist(series_tag):
dpg.set_value(series_tag, (downsampled_time, downsampled_values.astype(float)))
# update timeline
current_time_s = self.playback_manager.current_time_s
dpg.set_value(self.timeline_indicator_tag, [[current_time_s], [0]])
# update timeseries legend label
for series_path, (time_array, value_array) in self._series_data.items():
position = np.searchsorted(time_array, current_time_s, side='right') - 1
if position >= 0 and (current_time_s - time_array[position]) <= 1.0:
value = value_array[position]
formatted_value = f"{value:.5f}" if np.issubdtype(type(value), np.floating) else str(value)
series_tag = f"series_{self.panel_id}_{series_path}"
if dpg.does_item_exist(series_tag):
dpg.configure_item(series_tag, label=f"{series_path}: {formatted_value}")
def _on_x_axis_sync(self, min_time: float, max_time: float, source_panel):
with self._update_lock:
if source_panel != self:
self._queued_x_sync = (min_time, max_time)
def _fit_y_axis(self, x_min: float, x_max: float):
if not self._series_data:
dpg.set_axis_limits(self.y_axis_tag, -1, 1)
return
global_min = float('inf')
global_max = float('-inf')
found_data = False
for time_array, value_array in self._series_data.values():
if len(time_array) == 0:
continue
start_idx, end_idx = np.searchsorted(time_array, [x_min, x_max])
end_idx = min(end_idx, len(time_array) - 1)
if start_idx <= end_idx:
y_slice = value_array[start_idx:end_idx + 1]
series_min, series_max = np.min(y_slice), np.max(y_slice)
global_min = min(global_min, series_min)
global_max = max(global_max, series_max)
found_data = True
if not found_data:
dpg.set_axis_limits(self.y_axis_tag, -1, 1)
return
if global_min == global_max:
padding = max(abs(global_min) * 0.1, 1.0)
y_min, y_max = global_min - padding, global_max + padding
else:
range_size = global_max - global_min
padding = range_size * 0.1
y_min, y_max = global_min - padding, global_max + padding
dpg.set_axis_limits(self.y_axis_tag, y_min, y_max)
def _downsample_all_series(self, plot_duration):
plot_width = dpg.get_item_rect_size(self.plot_tag)[0]
if plot_width <= 0 or plot_duration <= 0:
return
self._last_plot_duration = plot_duration
target_points_per_second = plot_width / plot_duration
work_items = []
for series_path, (time_array, value_array) in self._series_data.items():
if len(time_array) == 0:
continue
series_duration = time_array[-1] - time_array[0] if len(time_array) > 1 else 1
points_per_second = len(time_array) / series_duration
if points_per_second > target_points_per_second * 2:
target_points = max(int(target_points_per_second * series_duration), plot_width)
work_items.append((series_path, time_array, value_array, target_points))
elif dpg.does_item_exist(f"series_{self.panel_id}_{series_path}"):
dpg.set_value(f"series_{self.panel_id}_{series_path}", (time_array, value_array.astype(float)))
if work_items:
self.worker_manager.submit_task(
TimeSeriesPanel._downsample_worker, work_items, callback=lambda results: self._results_deque.append(results), task_id=f"downsample_{self.panel_id}"
)
def add_series(self, series_path: str, update: bool = False):
with self._update_lock:
if update or series_path not in self._series_data:
self._series_data[series_path] = self.data_manager.get_timeseries(series_path)
time_array, value_array = self._series_data[series_path]
series_tag = f"series_{self.panel_id}_{series_path}"
if dpg.does_item_exist(series_tag):
dpg.set_value(series_tag, (time_array, value_array.astype(float)))
else:
line_series_tag = dpg.add_line_series(x=time_array, y=value_array.astype(float), label=series_path, parent=self.y_axis_tag, tag=series_tag)
dpg.bind_item_theme(line_series_tag, "line_theme")
self._fit_y_axis(*dpg.get_axis_limits(self.x_axis_tag))
plot_duration = dpg.get_axis_limits(self.x_axis_tag)[1] - dpg.get_axis_limits(self.x_axis_tag)[0]
self._downsample_all_series(plot_duration)
def destroy_ui(self):
with self._update_lock:
self.data_manager.remove_observer(self.on_data_loaded)
self.playback_manager.remove_x_axis_observer(self._on_x_axis_sync)
if dpg.does_item_exist(self.plot_tag):
dpg.delete_item(self.plot_tag)
self._ui_created = False
def get_panel_type(self) -> str:
return "timeseries"
def clear(self):
with self._update_lock:
for series_path in list(self._series_data.keys()):
self.remove_series(series_path)
def remove_series(self, series_path: str):
with self._update_lock:
if series_path in self._series_data:
if dpg.does_item_exist(f"series_{self.panel_id}_{series_path}"):
dpg.delete_item(f"series_{self.panel_id}_{series_path}")
del self._series_data[series_path]
def on_data_loaded(self, data: dict):
with self._update_lock:
self._new_data = True
if data.get('metadata_loaded'):
self._total_segments = data.get('total_segments', 0)
limits = (-10, self._total_segments * 60 + 10)
self._queued_x_sync = limits
def _on_series_drop(self, sender, app_data, user_data):
self.add_series(app_data)
@staticmethod
def _downsample_worker(series_path, time_array, value_array, target_points):
if len(time_array) <= target_points:
return series_path, time_array, value_array
step = len(time_array) / target_points
indices = []
for i in range(target_points):
start_idx = int(i * step)
end_idx = int((i + 1) * step)
if start_idx == end_idx:
indices.append(start_idx)
else:
bucket_values = value_array[start_idx:end_idx]
min_idx = start_idx + np.argmin(bucket_values)
max_idx = start_idx + np.argmax(bucket_values)
if min_idx != max_idx:
indices.extend([min(min_idx, max_idx), max(min_idx, max_idx)])
else:
indices.append(min_idx)
indices = sorted(set(indices))
return series_path, time_array[indices], value_array[indices]
+13 -4
View File
@@ -60,8 +60,16 @@ def cmd_download(args):
return return
try: try:
uf = URLFile(url, cache=False) # Stream the file in a single HTTP request instead of making
total = uf.get_length() # a separate Range request per chunk (which was very slow).
pool = URLFile.pool_manager()
r = pool.request("GET", url, preload_content=False)
if r.status not in (200, 206):
sys.stderr.write(f"ERROR:HTTP {r.status}\n")
sys.stderr.flush()
sys.exit(1)
total = int(r.headers.get('content-length', 0))
if total <= 0: if total <= 0:
sys.stderr.write("ERROR:File not found or empty\n") sys.stderr.write("ERROR:File not found or empty\n")
sys.stderr.flush() sys.stderr.flush()
@@ -73,8 +81,7 @@ def cmd_download(args):
downloaded = 0 downloaded = 0
chunk_size = 1024 * 1024 chunk_size = 1024 * 1024
with os.fdopen(tmp_fd, 'wb') as f: with os.fdopen(tmp_fd, 'wb') as f:
while downloaded < total: for data in r.stream(chunk_size):
data = uf.read(min(chunk_size, total - downloaded))
f.write(data) f.write(data)
downloaded += len(data) downloaded += len(data)
sys.stderr.write(f"PROGRESS:{downloaded}:{total}\n") sys.stderr.write(f"PROGRESS:{downloaded}:{total}\n")
@@ -91,6 +98,8 @@ def cmd_download(args):
except OSError: except OSError:
pass pass
raise raise
finally:
r.release_conn()
except Exception as e: except Exception as e:
sys.stderr.write(f"ERROR:{e}\n") sys.stderr.write(f"ERROR:{e}\n")
Generated
+58 -73
View File
@@ -116,12 +116,12 @@ wheels = [
[[package]] [[package]]
name = "bzip2" name = "bzip2"
version = "1.0.8" version = "1.0.8"
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=bzip2&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } source = { git = "https://github.com/commaai/dependencies.git?subdirectory=bzip2&rev=releases#9777ee38aa5ca9439843125392af38ed1262e500" }
[[package]] [[package]]
name = "capnproto" name = "capnproto"
version = "1.0.1" version = "1.0.1"
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=releases#9777ee38aa5ca9439843125392af38ed1262e500" }
[[package]] [[package]]
name = "casadi" name = "casadi"
@@ -359,16 +359,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/fa/d3c15189f7c52aaefbaea76fb012119b04b9013f4bf446cb4eb4c26c4e6b/cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c", size = 1257078, upload-time = "2026-01-04T14:14:12.373Z" }, { url = "https://files.pythonhosted.org/packages/ff/fa/d3c15189f7c52aaefbaea76fb012119b04b9013f4bf446cb4eb4c26c4e6b/cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c", size = 1257078, upload-time = "2026-01-04T14:14:12.373Z" },
] ]
[[package]]
name = "dearpygui"
version = "2.2"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/c8/b4afdac89c7bf458513366af3143f7383d7b09721637989c95788d93e24c/dearpygui-2.2-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:34ceae1ca1b65444e49012d6851312e44f08713da1b8cc0150cf41f1c207af9c", size = 1931443, upload-time = "2026-02-17T14:21:54.394Z" },
{ url = "https://files.pythonhosted.org/packages/43/93/a2d083b2e0edb095be815662cc41e40cf9ea7b65d6323e47bb30df7eb284/dearpygui-2.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:e1fae9ae59fec0e41773df64c80311a6ba67696219dde5506a2a4c013e8bcdfa", size = 2592645, upload-time = "2026-02-17T14:22:02.869Z" },
{ url = "https://files.pythonhosted.org/packages/80/ba/eae13acaad479f522db853e8b1ccd695a7bc8da2b9685c1d70a3b318df89/dearpygui-2.2-cp312-cp312-win_amd64.whl", hash = "sha256:7d399543b5a26ab6426ef3bbd776e55520b491b3e169647bde5e6b2de3701b35", size = 1830531, upload-time = "2026-02-17T14:21:43.386Z" },
]
[[package]] [[package]]
name = "dnspython" name = "dnspython"
version = "2.8.0" version = "2.8.0"
@@ -381,7 +371,7 @@ wheels = [
[[package]] [[package]]
name = "eigen" name = "eigen"
version = "3.4.0" version = "3.4.0"
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } source = { git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=releases#9777ee38aa5ca9439843125392af38ed1262e500" }
[[package]] [[package]]
name = "execnet" name = "execnet"
@@ -395,7 +385,7 @@ wheels = [
[[package]] [[package]]
name = "ffmpeg" name = "ffmpeg"
version = "7.1.0" version = "7.1.0"
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases#9777ee38aa5ca9439843125392af38ed1262e500" }
[[package]] [[package]]
name = "fonttools" name = "fonttools"
@@ -442,7 +432,7 @@ wheels = [
[[package]] [[package]]
name = "gcc-arm-none-eabi" name = "gcc-arm-none-eabi"
version = "13.2.1" version = "13.2.1"
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases#9777ee38aa5ca9439843125392af38ed1262e500" }
[[package]] [[package]]
name = "ghp-import" name = "ghp-import"
@@ -459,7 +449,7 @@ wheels = [
[[package]] [[package]]
name = "git-lfs" name = "git-lfs"
version = "3.6.1" version = "3.6.1"
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases#9777ee38aa5ca9439843125392af38ed1262e500" }
[[package]] [[package]]
name = "google-crc32c" name = "google-crc32c"
@@ -577,7 +567,7 @@ wheels = [
[[package]] [[package]]
name = "libjpeg" name = "libjpeg"
version = "3.1.0" version = "3.1.0"
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libjpeg&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libjpeg&rev=releases#9777ee38aa5ca9439843125392af38ed1262e500" }
[[package]] [[package]]
name = "libusb1" name = "libusb1"
@@ -593,7 +583,7 @@ wheels = [
[[package]] [[package]]
name = "libyuv" name = "libyuv"
version = "1922.0" version = "1922.0"
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libyuv&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libyuv&rev=releases#9777ee38aa5ca9439843125392af38ed1262e500" }
[[package]] [[package]]
name = "markdown" name = "markdown"
@@ -745,7 +735,7 @@ wheels = [
[[package]] [[package]]
name = "ncurses" name = "ncurses"
version = "6.5" version = "6.5"
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=releases#9777ee38aa5ca9439843125392af38ed1262e500" }
[[package]] [[package]]
name = "numpy" name = "numpy"
@@ -800,6 +790,7 @@ dependencies = [
{ name = "cython" }, { name = "cython" },
{ name = "eigen" }, { name = "eigen" },
{ name = "ffmpeg" }, { name = "ffmpeg" },
{ name = "gcc-arm-none-eabi" },
{ name = "git-lfs" }, { name = "git-lfs" },
{ name = "inputs" }, { name = "inputs" },
{ name = "jeepney" }, { name = "jeepney" },
@@ -809,7 +800,7 @@ dependencies = [
{ name = "libyuv" }, { name = "libyuv" },
{ name = "ncurses" }, { name = "ncurses" },
{ name = "numpy" }, { name = "numpy" },
{ name = "openssl3" }, { name = "pillow" },
{ name = "psutil" }, { name = "psutil" },
{ name = "pycapnp" }, { name = "pycapnp" },
{ name = "pycryptodome" }, { name = "pycryptodome" },
@@ -837,7 +828,6 @@ dependencies = [
[package.optional-dependencies] [package.optional-dependencies]
dev = [ dev = [
{ name = "gcc-arm-none-eabi" },
{ name = "matplotlib" }, { name = "matplotlib" },
{ name = "opencv-python-headless" }, { name = "opencv-python-headless" },
] ]
@@ -860,7 +850,6 @@ testing = [
{ name = "ty" }, { name = "ty" },
] ]
tools = [ tools = [
{ name = "dearpygui", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" },
{ name = "metadrive-simulator", marker = "platform_machine != 'aarch64'" }, { name = "metadrive-simulator", marker = "platform_machine != 'aarch64'" },
] ]
@@ -877,10 +866,9 @@ requires-dist = [
{ name = "coverage", marker = "extra == 'testing'" }, { name = "coverage", marker = "extra == 'testing'" },
{ name = "crcmod-plus" }, { name = "crcmod-plus" },
{ name = "cython" }, { name = "cython" },
{ name = "dearpygui", marker = "(platform_machine != 'aarch64' and extra == 'tools') or (sys_platform != 'linux' and extra == 'tools')", specifier = ">=2.1.0" },
{ name = "eigen", git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=releases" }, { name = "eigen", git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=releases" },
{ name = "ffmpeg", git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases" }, { name = "ffmpeg", git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=releases" },
{ name = "gcc-arm-none-eabi", marker = "extra == 'dev'", git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases" }, { name = "gcc-arm-none-eabi", git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=releases" },
{ name = "git-lfs", git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases" }, { name = "git-lfs", git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=releases" },
{ name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" },
{ name = "inputs" }, { name = "inputs" },
@@ -896,7 +884,7 @@ requires-dist = [
{ name = "ncurses", git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=releases" }, { name = "ncurses", git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=releases" },
{ name = "numpy", specifier = ">=2.0" }, { name = "numpy", specifier = ">=2.0" },
{ name = "opencv-python-headless", marker = "extra == 'dev'" }, { name = "opencv-python-headless", marker = "extra == 'dev'" },
{ name = "openssl3", git = "https://github.com/commaai/dependencies.git?subdirectory=openssl3&rev=releases" }, { name = "pillow" },
{ name = "pre-commit-hooks", marker = "extra == 'testing'" }, { name = "pre-commit-hooks", marker = "extra == 'testing'" },
{ name = "psutil" }, { name = "psutil" },
{ name = "pycapnp" }, { name = "pycapnp" },
@@ -932,11 +920,6 @@ requires-dist = [
] ]
provides-extras = ["docs", "testing", "dev", "tools"] provides-extras = ["docs", "testing", "dev", "tools"]
[[package]]
name = "openssl3"
version = "3.4.1"
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=openssl3&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" }
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "26.0" version = "26.0"
@@ -1306,7 +1289,7 @@ wheels = [
[[package]] [[package]]
name = "python3-dev" name = "python3-dev"
version = "3.12.8" version = "3.12.8"
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=python3-dev&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } source = { git = "https://github.com/commaai/dependencies.git?subdirectory=python3-dev&rev=releases#9777ee38aa5ca9439843125392af38ed1262e500" }
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
@@ -1449,15 +1432,15 @@ wheels = [
[[package]] [[package]]
name = "sentry-sdk" name = "sentry-sdk"
version = "2.53.0" version = "2.54.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
{ name = "urllib3" }, { name = "urllib3" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/d3/06/66c8b705179bc54087845f28fd1b72f83751b6e9a195628e2e9af9926505/sentry_sdk-2.53.0.tar.gz", hash = "sha256:6520ef2c4acd823f28efc55e43eb6ce2e6d9f954a95a3aa96b6fd14871e92b77", size = 412369, upload-time = "2026-02-16T11:11:14.743Z" } sdist = { url = "https://files.pythonhosted.org/packages/c8/e9/2e3a46c304e7fa21eaa70612f60354e32699c7102eb961f67448e222ad7c/sentry_sdk-2.54.0.tar.gz", hash = "sha256:2620c2575128d009b11b20f7feb81e4e4e8ae08ec1d36cbc845705060b45cc1b", size = 413813, upload-time = "2026-03-02T15:12:41.355Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/47/d4/2fdf854bc3b9c7f55219678f812600a20a138af2dd847d99004994eada8f/sentry_sdk-2.53.0-py2.py3-none-any.whl", hash = "sha256:46e1ed8d84355ae54406c924f6b290c3d61f4048625989a723fd622aab838899", size = 437908, upload-time = "2026-02-16T11:11:13.227Z" }, { url = "https://files.pythonhosted.org/packages/53/39/be412cc86bc6247b8f69e9383d7950711bd86f8d0a4a4b0fe8fad685bc21/sentry_sdk-2.54.0-py2.py3-none-any.whl", hash = "sha256:fd74e0e281dcda63afff095d23ebcd6e97006102cdc8e78a29f19ecdf796a0de", size = 439198, upload-time = "2026-03-02T15:12:39.546Z" },
] ]
[[package]] [[package]]
@@ -1553,26 +1536,26 @@ wheels = [
[[package]] [[package]]
name = "ty" name = "ty"
version = "0.0.19" version = "0.0.20"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/84/5e/da108b9eeb392e02ff0478a34e9651490b36af295881cb56575b83f0cc3a/ty-0.0.19.tar.gz", hash = "sha256:ee3d9ed4cb586e77f6efe3d0fe5a855673ca438a3d533a27598e1d3502a2948a", size = 5220026, upload-time = "2026-02-26T12:13:15.215Z" } sdist = { url = "https://files.pythonhosted.org/packages/56/95/8de69bb98417227b01f1b1d743c819d6456c9fd140255b6124b05b17dfd6/ty-0.0.20.tar.gz", hash = "sha256:ebba6be7974c14efbb2a9adda6ac59848f880d7259f089dfa72a093039f1dcc6", size = 5262529, upload-time = "2026-03-02T15:51:36.587Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/31/fd8c6067abb275bea11523d21ecf64e1d870b1ce80cac529cf6636df1471/ty-0.0.19-py3-none-linux_armv6l.whl", hash = "sha256:29bed05d34c8a7597567b8e327c53c1aed4a07dcfbe6c81e6d60c7444936ad77", size = 10268470, upload-time = "2026-02-26T12:13:42.881Z" }, { url = "https://files.pythonhosted.org/packages/0b/2c/718abe48393e521bf852cd6b0f984766869b09c258d6e38a118768a91731/ty-0.0.20-py3-none-linux_armv6l.whl", hash = "sha256:7cc12769c169c9709a829c2248ee2826b7aae82e92caeac813d856f07c021eae", size = 10333656, upload-time = "2026-03-02T15:51:56.461Z" },
{ url = "https://files.pythonhosted.org/packages/15/de/16a11bbf7d98c75849fc41f5d008b89bb5d080a4b10dc8ea851ee2bd371b/ty-0.0.19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:79140870c688c97ec68e723c28935ddef9d91a76d48c68e665fe7c851e628b8a", size = 10098562, upload-time = "2026-02-26T12:13:31.618Z" }, { url = "https://files.pythonhosted.org/packages/41/0e/eb1c4cc4a12862e2327b72657bcebb10b7d9f17046f1bdcd6457a0211615/ty-0.0.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b777c1bf13bc0a95985ebb8a324b8668a4a9b2e514dde5ccf09e4d55d2ff232", size = 10168505, upload-time = "2026-03-02T15:51:51.895Z" },
{ url = "https://files.pythonhosted.org/packages/e7/4f/086d6ff6686eadf903913c45b53ab96694b62bbfee1d8cf3e55a9b5aa4b2/ty-0.0.19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6e9c1f9cfa6a26f7881d14d75cf963af743f6c4189e6aa3e3b4056a65f22e730", size = 9604073, upload-time = "2026-02-26T12:13:24.645Z" }, { url = "https://files.pythonhosted.org/packages/89/7f/10230798e673f0dd3094dfd16e43bfd90e9494e7af6e8e7db516fb431ddf/ty-0.0.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b2a4a7db48bf8cba30365001bc2cad7fd13c1a5aacdd704cc4b7925de8ca5eb3", size = 9678510, upload-time = "2026-03-02T15:51:48.451Z" },
{ url = "https://files.pythonhosted.org/packages/95/13/888a6b6c7ed4a880fee91bec997f775153ce86215ee4c56b868516314734/ty-0.0.19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbca43b050edf1db2e64ae7b79add233c2aea2855b8a876081bbd032edcd0610", size = 10106295, upload-time = "2026-02-26T12:13:40.584Z" }, { url = "https://files.pythonhosted.org/packages/7a/3d/59d9159577494edd1728f7db77b51bb07884bd21384f517963114e3ab5f6/ty-0.0.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6846427b8b353a43483e9c19936dc6a25612573b44c8f7d983dfa317e7f00d4c", size = 10162926, upload-time = "2026-03-02T15:51:40.558Z" },
{ url = "https://files.pythonhosted.org/packages/cb/e8/05a372cae8da482de73b8246fb43236bf11e24ac28c879804568108759db/ty-0.0.19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8acaa88ab1955ca6b15a0ccc274011c4961377fe65c3948e5d2b212f2517b87c", size = 10098234, upload-time = "2026-02-26T12:13:33.725Z" }, { url = "https://files.pythonhosted.org/packages/9c/a8/b7273eec3e802f78eb913fbe0ce0c16ef263723173e06a5776a8359b2c66/ty-0.0.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:245ceef5bd88df366869385cf96411cb14696334f8daa75597cf7e41c3012eb8", size = 10171702, upload-time = "2026-03-02T15:51:44.069Z" },
{ url = "https://files.pythonhosted.org/packages/c5/f1/5b0958e9e9576e7662192fe689bbb3dc88e631a4e073db3047793a547d58/ty-0.0.19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a901b6a6dd9d17d5b3b2e7bafc3057294e88da3f5de507347316687d7f191a1", size = 10607218, upload-time = "2026-02-26T12:13:17.576Z" }, { url = "https://files.pythonhosted.org/packages/9f/32/5f1144f2f04a275109db06e3498450c4721554215b80ae73652ef412eeab/ty-0.0.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4d21d1cdf67a444d3c37583c17291ddba9382a9871021f3f5d5735e09e85efe", size = 10682552, upload-time = "2026-03-02T15:51:33.102Z" },
{ url = "https://files.pythonhosted.org/packages/fb/ab/358c78b77844f58ff5aca368550ab16c719f1ab0ec892ceb1114d7500f4e/ty-0.0.19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8deafdaaaee65fd121c66064da74a922d8501be4a2d50049c71eab521a23eff7", size = 11160593, upload-time = "2026-02-26T12:13:36.008Z" }, { url = "https://files.pythonhosted.org/packages/6a/db/9f1f637310792f12bd6ed37d5fc8ab39ba1a9b0c6c55a33865e9f1cad840/ty-0.0.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd4ffd907d1bd70e46af9e9a2f88622f215e1bf44658ea43b32c2c0b357299e4", size = 11242605, upload-time = "2026-03-02T15:51:34.895Z" },
{ url = "https://files.pythonhosted.org/packages/95/59/827fc346d66a59fe48e9689a5ceb67dbbd5b4de2e8d4625371af39a2e8b7/ty-0.0.19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e56071af280897441018f74f921b97d53aec0856f8af85f4f949df8eda07d", size = 10822392, upload-time = "2026-02-26T12:13:29.415Z" }, { url = "https://files.pythonhosted.org/packages/1a/68/cc9cae2e732fcfd20ccdffc508407905a023fc8493b8771c392d915528dc/ty-0.0.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6594b58d8b0e9d16a22b3045fc1305db4b132c8d70c17784ab8c7a7cc986807", size = 10974655, upload-time = "2026-03-02T15:51:46.011Z" },
{ url = "https://files.pythonhosted.org/packages/81/f9/3bbfbbe35478de9bcd63848f4bc9bffda72278dd9732dbad3efc3978432e/ty-0.0.19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abdf5885130393ce74501dba792f48ce0a515756ec81c33a4b324bdf3509df6e", size = 10707139, upload-time = "2026-02-26T12:13:20.148Z" }, { url = "https://files.pythonhosted.org/packages/1c/c1/b9e3e3f28fe63486331e653f6aeb4184af8b1fe80542fcf74d2dda40a93d/ty-0.0.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3662f890518ce6cf4d7568f57d03906912d2afbf948a01089a28e325b1ef198c", size = 10761325, upload-time = "2026-03-02T15:51:26.818Z" },
{ url = "https://files.pythonhosted.org/packages/12/9e/597023b183ec4ade83a36a0cea5c103f3bffa34f70813d46386c61447fb8/ty-0.0.19-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:877e89005c8f9d1dbff5ad14cbac9f35c528406fde38926f9b44f24830de8d6a", size = 10096933, upload-time = "2026-02-26T12:13:45.266Z" }, { url = "https://files.pythonhosted.org/packages/39/9e/67db935bdedf219a00fb69ec5437ba24dab66e0f2e706dd54a4eca234b84/ty-0.0.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e3ffbae58f9f0d17cdc4ac6d175ceae560b7ed7d54f9ddfb1c9f31054bcdc2c", size = 10145793, upload-time = "2026-03-02T15:51:38.562Z" },
{ url = "https://files.pythonhosted.org/packages/1e/76/d0d2f6e674db2a17c8efa5e26682b9dfa8d34774705f35902a7b45ebd3bd/ty-0.0.19-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:39bd1da051c1e4d316efaf79dbed313255633f7c6ad6e24d29f4d9c6ffaf4de6", size = 10109547, upload-time = "2026-02-26T12:13:22.17Z" }, { url = "https://files.pythonhosted.org/packages/c7/de/b0eb815d4dc5a819c7e4faddc2a79058611169f7eef07ccc006531ce228c/ty-0.0.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:176e52bc8bb00b0e84efd34583962878a447a3a0e34ecc45fd7097a37554261b", size = 10189640, upload-time = "2026-03-02T15:51:50.202Z" },
{ url = "https://files.pythonhosted.org/packages/a4/b0/76026c06b852a3aa4fdb5bd329fdc2175aaf3c64a3fafece9cc4df167cee/ty-0.0.19-py3-none-musllinux_1_2_i686.whl", hash = "sha256:87df8415a6c9cb27b8f1382fcdc6052e59f5b9f50f78bc14663197eb5c8d3699", size = 10289110, upload-time = "2026-02-26T12:13:38.29Z" }, { url = "https://files.pythonhosted.org/packages/b8/71/63734923965cbb70df1da3e93e4b8875434e326b89e9f850611122f279bf/ty-0.0.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2bc73025418e976ca4143dde71fb9025a90754a08ac03e6aa9b80d4bed1294b", size = 10370568, upload-time = "2026-03-02T15:51:42.295Z" },
{ url = "https://files.pythonhosted.org/packages/14/6c/f3b3a189816b4f079b20fe5d0d7ee38e38a472f53cc6770bb6571147e3de/ty-0.0.19-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:89b6bb23c332ed5c38dd859eb5793f887abcc936f681a40d4ea68e35eac1af33", size = 10796479, upload-time = "2026-02-26T12:13:10.992Z" }, { url = "https://files.pythonhosted.org/packages/32/a0/a532c2048533347dff48e9ca98bd86d2c224356e101688a8edaf8d6973fb/ty-0.0.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d52f7c9ec6e363e094b3c389c344d5a140401f14a77f0625e3f28c21918552f5", size = 10853999, upload-time = "2026-03-02T15:51:58.963Z" },
{ url = "https://files.pythonhosted.org/packages/3d/18/caee33d1ce9dd50bd94c26cde7cda4f6971e22e474e7d72a5c86d745ad58/ty-0.0.19-py3-none-win32.whl", hash = "sha256:19b33df3aa7af7b1a9eaa4e1175c3b4dec0f5f2e140243e3492c8355c37418f3", size = 9677215, upload-time = "2026-02-26T12:13:08.519Z" }, { url = "https://files.pythonhosted.org/packages/48/88/36c652c658fe96658043e4abc8ea97801de6fb6e63ab50aaa82807bff1d8/ty-0.0.20-py3-none-win32.whl", hash = "sha256:c7d32bfe93f8fcaa52b6eef3f1b930fd7da410c2c94e96f7412c30cfbabf1d17", size = 9744206, upload-time = "2026-03-02T15:51:54.183Z" },
{ url = "https://files.pythonhosted.org/packages/81/41/18fc0771d0b1da7d7cc2fc9af278d3122b754fe8b521a748734f4e16ecfd/ty-0.0.19-py3-none-win_amd64.whl", hash = "sha256:b9052c61464cdd76bc8e6796f2588c08700f25d0dcbc225bb165e390ea9d96a4", size = 10651252, upload-time = "2026-02-26T12:13:13.035Z" }, { url = "https://files.pythonhosted.org/packages/ff/a7/a4a13bed1d7fd9d97aaa3c5bb5e6d3e9a689e6984806cbca2ab4c9233cac/ty-0.0.20-py3-none-win_amd64.whl", hash = "sha256:a5e10f40fc4a0a1cbcb740a4aad5c7ce35d79f030836ea3183b7a28f43170248", size = 10711999, upload-time = "2026-03-02T15:51:29.212Z" },
{ url = "https://files.pythonhosted.org/packages/8b/8c/26f7ce8863eb54510082747b3dfb1046ba24f16fc11de18c0e5feb36ff18/ty-0.0.19-py3-none-win_arm64.whl", hash = "sha256:9329804b66dcbae8e7af916ef4963221ed53b8ec7d09b0793591c5ae8a0f3270", size = 10093195, upload-time = "2026-02-26T12:13:26.816Z" }, { url = "https://files.pythonhosted.org/packages/8d/7e/6bfd748a9f4ff9267ed3329b86a0f02cdf6ab49f87bc36c8a164852f99fc/ty-0.0.20-py3-none-win_arm64.whl", hash = "sha256:53f7a5c12c960e71f160b734f328eff9a35d578af4b67a36b0bb5990ac5cdc27", size = 10150143, upload-time = "2026-03-02T15:51:31.283Z" },
] ]
[[package]] [[package]]
@@ -1643,38 +1626,40 @@ wheels = [
[[package]] [[package]]
name = "yarl" name = "yarl"
version = "1.22.0" version = "1.23.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "idna" }, { name = "idna" },
{ name = "multidict" }, { name = "multidict" },
{ name = "propcache" }, { name = "propcache" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" },
{ url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" },
{ url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" },
{ url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" },
{ url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" },
{ url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" },
{ url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" },
{ url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" },
{ url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" },
{ url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" },
{ url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" },
{ url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" },
{ url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" },
{ url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" },
{ url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" },
{ url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" },
{ url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" },
{ url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" },
{ url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" },
] ]
[[package]] [[package]]
name = "zeromq" name = "zeromq"
version = "4.3.5" version = "4.3.5"
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=releases#9777ee38aa5ca9439843125392af38ed1262e500" }
[[package]] [[package]]
name = "zstandard" name = "zstandard"
@@ -1704,4 +1689,4 @@ wheels = [
[[package]] [[package]]
name = "zstd" name = "zstd"
version = "1.5.6" version = "1.5.6"
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zstd&rev=releases#12581f30b45b570dd0bbc36055fe1532f5a8ef60" } source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zstd&rev=releases#9777ee38aa5ca9439843125392af38ed1262e500" }