Compare commits

..

49 Commits

Author SHA1 Message Date
royjr
0491242b4a Merge branch 'master' into visuals-hide-camera 2026-03-15 15:14:12 -04:00
Jason Wen
37ac33fbcc gitignore: add CLAUDE.md and SKILL.md 2026-03-13 19:19:37 -04:00
James Vecellio-Grant
0376660023 ci: modify models repo title (#1764) 2026-03-13 13:19:45 -04:00
Jason Wen
2e82908c07 pandad: always prioritize internal panda (#1759)
* pandad: filter out external panda

* fix

* internal panda

* move it even higher

* this

* should be this still

* anoter

* more

* 1 more time

* bruh

* try this out

* revert

* gotta do this after

* filter
2026-03-10 20:30:25 -04:00
Jason Wen
b71914e006 [TIZI/TICI] ui: branch switcher is always available (#1762) 2026-03-07 01:48:35 -05:00
Jason Wen
a9d5c9e23a ui: add new timer options for Onroad Brightness Delay (#1760)
* ui: add new timer options for OnOnroad Brightness Delay

* migrate

* in future pr

* Revert "in future pr"

This reverts commit ca9940f809.

* consolidate

* update

* gate

* fix
2026-03-07 01:36:24 -05:00
Jason Wen
c01719bb99 ui: gate Onroad Brightness Delay on readiness (#1761)
ui: gate Onroad Brightness Timer on readiness
2026-03-06 23:38:38 -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
1b89608ccc Merge branch 'master' into visuals-hide-camera 2026-03-02 02:42:15 -05: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
53a24655d2 move to visuals 2026-02-13 22:18:08 -05:00
royjr
c9f92a8c76 no mici toggle 2026-02-13 22:11:36 -05:00
royjr
10b1d673c9 Apply description suggestion from @sunnyhaibin
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-02-13 22:10:02 -05:00
Jason Wen
7080167daf Merge branch 'master' into visuals-hide-camera 2026-02-13 17:03:34 -05:00
Jason Wen
c7a1c70504 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into visuals-hide-camera
# Conflicts:
#	selfdrive/ui/sunnypilot/ui_state.py
2026-02-13 16:55:01 -05:00
royjr
c6a6caf6ff Merge branch 'master' into visuals-hide-camera 2026-02-05 00:52:57 -05:00
royjr
8d49a44f52 Merge branch 'master' into visuals-hide-camera 2025-12-31 15:09:29 -05:00
royjr
3434ca9d3e bool 2025-12-31 14:19:32 -05:00
royjr
e4f8a5edd1 Update params_keys.h 2025-12-29 14:45:47 -05:00
royjr
1f4f9bd4bd big ui 2025-12-29 02:00:42 -05:00
royjr
455e730c4c simpler 2025-12-29 01:53:12 -05:00
royjr
b243d4e356 fix param 2025-12-29 01:12:00 -05:00
royjr
de0550d47b init 2025-12-29 01:06:47 -05:00
115 changed files with 1549 additions and 3555 deletions

View File

@@ -34,10 +34,10 @@ jobs:
echo "tinygrad_ref=$ref" >> $GITHUB_OUTPUT
echo "tinygrad_ref is $ref"
- name: Checkout docs repo (sunnypilot-docs, gh-pages)
- name: Checkout docs repo (sunnypilot-models, gh-pages)
uses: actions/checkout@v4
with:
repository: sunnypilot/sunnypilot-docs
repository: sunnypilot/sunnypilot-models
ref: gh-pages
path: docs
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
@@ -202,7 +202,7 @@ jobs:
- name: Checkout docs repo
uses: actions/checkout@v4
with:
repository: sunnypilot/sunnypilot-docs
repository: sunnypilot/sunnypilot-models
ref: gh-pages
path: docs
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}

View File

@@ -119,7 +119,7 @@ jobs:
- name: Checkout docs repo
uses: actions/checkout@v4
with:
repository: sunnypilot/sunnypilot-docs
repository: sunnypilot/sunnypilot-models
ref: gh-pages
path: docs
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}

View File

@@ -22,7 +22,7 @@ jobs:
running-workflow-name: 'build __nightly'
repo-token: ${{ secrets.GITHUB_TOKEN }}
check-regexp: ^((?!.*(build prebuilt|create badges).*).)*$
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 0

2
.gitignore vendored
View File

@@ -101,6 +101,8 @@ Pipfile
.context/
PLAN.md
TASK.md
CLAUDE.md
SKILL.md
### JetBrains ###
!.idea/customTargets.xml

View File

@@ -46,11 +46,10 @@ if arch != "larch64":
import libjpeg
import libyuv
import ncurses
import openssl3
import python3_dev
import zeromq
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
else:
# TODO: remove when AGNOS has our new vendor pkgs

View File

@@ -25,7 +25,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"CarParamsPersistent", {PERSISTENT, BYTES}},
{"CarParamsPrevRoute", {PERSISTENT, BYTES}},
{"CompletedTrainingVersion", {PERSISTENT, STRING, "0"}},
{"ConfidenceVisual", {PERSISTENT | BACKUP, INT, "0"}},
{"ControlsReady", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"CurrentBootlog", {PERSISTENT, STRING}},
{"CurrentRoute", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}},
@@ -55,6 +54,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"GsmRoaming", {PERSISTENT | BACKUP, BOOL}},
{"HardwareSerial", {PERSISTENT, STRING}},
{"HasAcceptedTerms", {PERSISTENT, STRING, "0"}},
{"HideCamera", {PERSISTENT | BACKUP, BOOL, "0"}},
{"InstallDate", {PERSISTENT, TIME}},
{"IsDriverViewEnabled", {CLEAR_ON_MANAGER_START, BOOL}},
{"IsEngaged", {PERSISTENT, BOOL}},
@@ -173,6 +173,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"OnroadScreenOffBrightness", {PERSISTENT | BACKUP, INT, "0"}},
{"OnroadScreenOffBrightnessMigrated", {PERSISTENT | BACKUP, STRING, "0.0"}},
{"OnroadScreenOffTimer", {PERSISTENT | BACKUP, INT, "15"}},
{"OnroadScreenOffTimerMigrated", {PERSISTENT | BACKUP, STRING, "0.0"}},
{"OnroadUploads", {PERSISTENT | BACKUP, BOOL, "1"}},
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},

View File

@@ -32,12 +32,12 @@ dependencies = [
"ffmpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ffmpeg",
"libjpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=libjpeg",
"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",
"zstd @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zstd",
"ncurses @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ncurses",
"zeromq @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zeromq",
"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
"av",
@@ -76,6 +76,7 @@ dependencies = [
"raylib > 5.5.0.3",
"qrcode",
"jeepney",
"pillow",
]
[project.optional-dependencies]
@@ -103,12 +104,10 @@ testing = [
dev = [
"matplotlib",
"opencv-python-headless",
"gcc-arm-none-eabi @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=gcc-arm-none-eabi",
]
tools = [
"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]

View File

@@ -12,7 +12,7 @@ from openpilot.common.basedir import BASEDIR
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'

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -32,8 +32,7 @@ def flash_panda(panda_serial: str) -> Panda:
raise
# skip flashing if the detected panda is not supported
supported_panda = check_panda_support(panda)
if not supported_panda:
if panda.get_type() not in Panda.SUPPORTED_DEVICES:
cloudlog.warning(f"Panda {panda_serial} is not supported (hw_type: {panda.get_type()}), skipping flash...")
return panda
@@ -69,12 +68,20 @@ def flash_panda(panda_serial: str) -> Panda:
return panda
def check_panda_support(panda) -> bool:
hw_type = panda.get_type()
if hw_type in Panda.SUPPORTED_DEVICES:
return True
def check_panda_support(panda_serials: list[str]) -> list[str]:
spi_serials = set(Panda.spi_list())
for serial in panda_serials:
if serial in spi_serials:
return [serial]
return False
for serial in panda_serials:
panda = Panda(serial)
is_internal = panda.is_internal()
panda.close()
if is_internal:
return [serial]
return []
def main() -> None:
@@ -126,13 +133,18 @@ def main() -> None:
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)
# find the internal supported panda (e.g. skip external Black Panda)
panda_serials = check_panda_support(panda_serials)
if len(panda_serials) == 0:
continue
# Flash the first panda
panda_serial = panda_serials[0]
panda = flash_panda(panda_serial)
# flash Rivian longitudinal upgrade panda
flash_rivian_long(panda)
# Ensure internal panda is present if expected
if HARDWARE.has_internal_panda() and not panda.is_internal():
cloudlog.error("Internal panda is missing, trying again")
@@ -143,12 +155,6 @@ def main() -> None:
# log panda fw version
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
health = panda.health()
if health["heartbeat_lost"]:

View File

@@ -81,16 +81,14 @@ void run(const char* cmd) {
}
void finishInstall() {
BeginDrawing();
ClearBackground(BLACK);
if (tici_device) {
if (tici_device) {
BeginDrawing();
ClearBackground(BLACK);
const char *m = "Finishing install...";
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);
} else {
DrawTextEx(font_display, "finishing setup", (Vector2){12, 0}, 77, 0, (Color){255, 255, 255, (unsigned char)(255 * 0.9)});
}
EndDrawing();
EndDrawing();
}
util::sleep_for(60 * 1000);
}

View File

@@ -69,7 +69,6 @@ class SoftwareLayout(Widget):
# Branch switcher
self._branch_btn = button_item(lambda: tr("Target Branch"), lambda: tr("SELECT"), callback=self._on_select_branch)
self._branch_btn.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
self._branch_btn.action_item.set_value(ui_state.params.get("UpdaterTargetBranch") or "")
self._branch_dialog: MultiOptionDialog | None = None

View File

@@ -56,7 +56,7 @@ class MiciMainLayout(Scroller):
gui_app.push_widget(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:
gui_app.push_widget(self._onboarding_window)
@@ -82,7 +82,7 @@ class MiciMainLayout(Scroller):
def _handle_transitions(self):
# Don't pop if onboarding
if gui_app.get_active_widget() == self._onboarding_window:
if gui_app.widget_in_stack(self._onboarding_window):
return
if ui_state.started != self._prev_onroad:
@@ -108,7 +108,7 @@ class MiciMainLayout(Scroller):
def _on_interactive_timeout(self):
# Don't pop if onboarding
if gui_app.get_active_widget() == self._onboarding_window:
if gui_app.widget_in_stack(self._onboarding_window):
return
if ui_state.started:

View File

@@ -1,32 +1,25 @@
from enum import IntEnum
import weakref
import math
import numpy as np
import qrcode
import pyray as rl
from collections.abc import Callable
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.widgets import Widget
from openpilot.system.ui.widgets.button import SmallButton, SmallCircleIconButton
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.slider import SmallSlider
from openpilot.system.ui.mici_setup import TermsHeader, TermsPage as SetupTermsPage
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.button import SmallCircleIconButton
from openpilot.system.ui.widgets.scroller import NavScroller, Scroller
from openpilot.system.ui.widgets.nav_widget import NavWidget
from openpilot.system.ui.mici_setup import GreyBigButton, BigPillButton
from openpilot.system.ui.widgets.label import gui_label
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.version import terms_version, training_version, terms_version_sp
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkOnboarding
class OnboardingState(IntEnum):
TERMS = 0
ONBOARDING = 1
DECLINE = 2
SUNNYLINK_CONSENT = 3
from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined
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
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import BaseDriverCameraDialog
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkConsentPage
class DriverCameraSetupDialog(BaseDriverCameraDialog):
@@ -60,91 +53,62 @@ class DriverCameraSetupDialog(BaseDriverCameraDialog):
rl.end_scissor_mode()
class TrainingGuidePreDMTutorial(SetupTermsPage):
def __init__(self, continue_callback):
super().__init__(continue_callback, continue_text="continue")
self._title_header = TermsHeader("driver monitoring setup", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
class TrainingGuidePreDMTutorial(NavScroller):
def __init__(self, continue_callback: Callable[[], None]):
super().__init__()
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, " +
"unplug and remount before continuing.", 42,
FontWeight.ROMAN)
continue_button = BigPillButton("next")
continue_button.set_click_callback(continue_callback)
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):
super().show_event()
# Get driver monitoring model ready for next step
ui_state.params.put_bool("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)),
))
ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", True)
class DMBadFaceDetected(SetupTermsPage):
def __init__(self, continue_callback, back_callback):
super().__init__(continue_callback, back_callback, continue_text="power off")
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)
class DMBadFaceDetected(NavScroller):
def __init__(self):
super().__init__()
@property
def _content_height(self):
return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset()
back_button = BigPillButton("back")
back_button.set_click_callback(self.dismiss)
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)),
))
self._scroller.add_widgets([
GreyBigButton("looking for driver", "make sure comma\nfour can see your face",
gui_app.texture("icons_mici/setup/orange_dm.png", 64, 64)),
GreyBigButton("", "Remount if your face is blocked, or driver monitoring has difficulty tracking your face."),
back_button,
])
class TrainingGuideDMTutorial(Widget):
class TrainingGuideDMTutorial(NavWidget):
PROGRESS_DURATION = 4
LOOKING_THRESHOLD_DEG = 30.0
def __init__(self, continue_callback):
def __init__(self, continue_callback: Callable[[], None]):
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.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.set_touch_valid_callback(lambda: self.enabled and not self.is_dismissing) # for nav stack
# Wrap the continue callback to restore settings
def wrapped_continue_callback():
device.set_offroad_brightness(None)
continue_callback()
self._good_button.set_click_callback(wrapped_continue_callback)
self._good_button.set_click_callback(continue_callback)
self._good_button.set_enabled(False)
self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps)
self._dialog = DriverCameraSetupDialog()
self._bad_face_page = DMBadFaceDetected(HARDWARE.shutdown, lambda: self_ref() and self_ref()._hide_bad_face_page())
self._should_show_bad_face_page = False
self._bad_face_page = DMBadFaceDetected()
# Disable driver monitoring model when device times out for inactivity
def inactivity_callback():
@@ -152,23 +116,11 @@ class TrainingGuideDMTutorial(Widget):
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):
super().show_event()
self._dialog.show_event()
self._progress.x = 0.0
device.set_offroad_brightness(100)
def _update_state(self):
super()._update_state()
if device.awake and not ui_state.params.get_bool("IsDriverViewEnabled"):
@@ -188,7 +140,8 @@ class TrainingGuideDMTutorial(Widget):
looking_center = False
# 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
duration = self.PROGRESS_DURATION * 2 if slow else self.PROGRESS_DURATION
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)
def _render(self, _):
if self._should_show_bad_face_page:
return self._bad_face_page.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),
@@ -258,266 +208,246 @@ class TrainingGuideDMTutorial(Widget):
))
# 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.end_scissor_mode()
class TrainingGuideRecordFront(SetupTermsPage):
def __init__(self, continue_callback):
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):
class TrainingGuideRecordFront(NavScroller):
def __init__(self, continue_callback: Callable[[], None]):
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():
if obj := self_ref():
obj._advance_step()
gui_app.push_widget(BigConfirmationDialogV2("allow data uploading", "icons_mici/setup/driver_monitoring/dm_check.png", exit_on_confirm=False,
confirm_callback=on_accept))
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 = [
TrainingGuideAttentionNotice(continue_callback=on_continue),
TrainingGuidePreDMTutorial(continue_callback=on_continue),
TrainingGuideDMTutorial(continue_callback=on_continue),
TrainingGuideRecordFront(continue_callback=on_continue),
TrainingGuideAttentionNotice(continue_callback=lambda: gui_app.push_widget(self._steps[1])),
TrainingGuidePreDMTutorial(continue_callback=lambda: gui_app.push_widget(self._steps[2])),
TrainingGuideDMTutorial(continue_callback=lambda: gui_app.push_widget(self._steps[3])),
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):
super().show_event()
device.set_override_interactive_timeout(300)
self._steps[0].show_event()
def hide_event(self):
super().hide_event()
device.set_override_interactive_timeout(None)
def _render(self, _):
self._steps[0].render(self._rect)
def _advance_step(self):
if self._step < len(self._steps) - 1:
self._step += 1
self._steps[self._step].show_event()
else:
self._step = 0
if self._completed_callback:
self._completed_callback()
class QRCodeWidget(Widget):
def __init__(self, url: str, size: int = 170):
super().__init__()
self.set_rect(rl.Rectangle(0, 0, size, size))
self._size = size
self._qr_texture: rl.Texture | None = None
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, _):
rl.draw_rectangle_rec(self._rect, rl.BLACK)
if self._step < len(self._steps):
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)),
))
super()._render(_)
class OnboardingWindow(Widget):
def __init__(self):
def __init__(self, completed_callback: Callable[[], None]):
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._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._decline_page = DeclinePage(back_callback=self._on_decline_back)
self._training_guide.set_enabled(lambda: self.enabled) # for nav stack
# sunnylink consent pages
self._accepted_terms = self._accepted_terms and ui_state.params.get("HasAcceptedTermsSP") == terms_version_sp
self._sunnylink = SunnylinkOnboarding()
if not self._accepted_terms:
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
self._needs_initial_push = False
def _on_uninstall(self):
ui_state.params.put_bool("DoUninstall", True)
def show_event(self):
super().show_event()
device.set_override_interactive_timeout(300)
device.set_offroad_brightness(100)
self._needs_initial_push = True
def hide_event(self):
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_offroad_brightness(None)
@property
def completed(self) -> bool:
return self._accepted_terms and self._sunnylink.completed and self._training_done
def _on_terms_declined(self):
self._state = OnboardingState.DECLINE
def _on_decline_back(self):
self._state = OnboardingState.TERMS
return self._accepted_terms and self._sunnylink_consent_done and self._training_done
def close(self):
ui_state.params.put_bool("IsDriverViewEnabled", False)
gui_app.pop_widget()
ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", False)
self._completed_callback()
def _on_terms_accepted(self):
ui_state.params.put("HasAcceptedTerms", terms_version)
ui_state.params.put("HasAcceptedTermsSP", terms_version_sp)
if not self._sunnylink.completed:
self._state = OnboardingState.SUNNYLINK_CONSENT
self._accepted_terms = True
if not self._sunnylink_consent_done:
gui_app.push_widget(self._sunnylink_consent)
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:
self.close()
def _on_completed_training(self):
ui_state.params.put("CompletedTrainingVersion", training_version)
self._training_done = True
self.close()
def _render(self, _):
rl.draw_rectangle_rec(self._rect, rl.BLACK)
if self._state == OnboardingState.TERMS:
self._terms.render(self._rect)
elif self._state == OnboardingState.SUNNYLINK_CONSENT:
self._sunnylink.render(self._rect)
if self._sunnylink.completed:
if not self._training_done:
self._state = OnboardingState.ONBOARDING
else:
self.close()
elif self._state == OnboardingState.ONBOARDING:
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)
# Deferred from show_event to avoid nested push_widget re-enable bug
if self._needs_initial_push:
self._needs_initial_push = False
if self._accepted_terms and not self._sunnylink_consent_done:
gui_app.push_widget(self._sunnylink_consent)
elif self._accepted_terms and self._sunnylink_consent_done and not self._training_done:
gui_app.push_widget(self._training_guide)
self._terms.render(self._rect)

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.onroad.driver_camera_dialog import DriverCameraDialog
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.multilang import tr
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.html_render import HtmlModal, HtmlRenderer
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):
def __init__(self, file_path: str | None = None, text: str | None = None):
super().__init__()
@@ -311,11 +335,11 @@ class DeviceLayoutMici(NavScroller):
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.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())
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())
self._scroller.add_widgets([

View File

@@ -1,13 +1,9 @@
import pyray as rl
from openpilot.system.ui.widgets.scroller import NavScroller
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon
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.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiIcon
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
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):
@@ -62,148 +58,3 @@ class WifiNetworkButton(BigButton):
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
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'))

View File

@@ -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'))

View File

@@ -2,7 +2,7 @@ from openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller import NavScroller
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.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.developer import DeveloperLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout

View File

@@ -231,7 +231,7 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
self._alpha_filter.update(0 if alert is None else 1)
if gui_app.sunnypilot_ui():
ui_state.onroad_brightness_handle_alerts(ui_state.started, alert)
ui_state.onroad_brightness_handle_alerts(ui_state, alert)
if alert is None:
# If still animating out, keep the previous alert

View File

@@ -212,6 +212,8 @@ class AugmentedRoadView(CameraView):
# Render the base camera view
super()._render(self._content_rect)
if ui_state.hide_camera:
rl.draw_rectangle_rec(self._content_rect, rl.BLACK)
# Draw all UI overlays
self._model_renderer.render(self._content_rect)
@@ -251,7 +253,7 @@ class AugmentedRoadView(CameraView):
# Draw darkened background and text if not onroad
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))
self._offroad_label.render(self._content_rect)
self._offroad_label.render(self._rect)
# publish uiDebug
msg = messaging.new_message('uiDebug')

View File

@@ -155,11 +155,11 @@ class CameraView(Widget):
# 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
# and only clears internal buffers, not the message queue.
self.frame = None
self.available_streams.clear()
if self.client:
del self.client
self.client = VisionIpcClient(self._name, self._stream_type, conflate=True)
self.frame = None
def _set_placeholder_color(self, color: rl.Color):
"""Set a placeholder color to be drawn when no frame is available."""

View File

@@ -24,12 +24,10 @@ def draw_circle_gradient(center_x: float, center_y: float, radius: int,
class ConfidenceBall(Widget, ConfidenceBallSP):
def __init__(self, demo: bool = False, scale: float = 1.0, visual: int = 0):
def __init__(self, demo: bool = False):
Widget.__init__(self)
ConfidenceBallSP.__init__(self)
self._demo = demo
self._scale = scale
self._visual = visual
self._confidence_filter = FirstOrderFilter(-0.5, 0.5, 1 / gui_app.target_fps)
def update_filter(self, value: float):
@@ -56,7 +54,7 @@ class ConfidenceBall(Widget, ConfidenceBallSP):
self.rect.height,
)
status_dot_radius = int(24 * self._scale)
status_dot_radius = 24
dot_height = (1 - self._confidence_filter.x) * (content_rect.height - 2 * status_dot_radius) + status_dot_radius
dot_height = self._rect.y + dot_height
@@ -83,7 +81,6 @@ class ConfidenceBall(Widget, ConfidenceBallSP):
top_dot_color = rl.Color(50, 50, 50, 255)
bottom_dot_color = rl.Color(13, 13, 13, 255)
if not self.update_confidence_visual(content_rect, status_dot_radius, dot_height, top_dot_color, bottom_dot_color):
draw_circle_gradient(content_rect.x + content_rect.width - status_dot_radius,
dot_height, status_dot_radius,
top_dot_color, bottom_dot_color)
draw_circle_gradient(content_rect.x + content_rect.width - status_dot_radius,
dot_height, status_dot_radius,
top_dot_color, bottom_dot_color)

View File

@@ -172,8 +172,7 @@ class HudRenderer(Widget):
def _render(self, rect: rl.Rectangle) -> None:
"""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:
self._draw_set_speed(rect)

View File

@@ -145,6 +145,9 @@ def arc_bar_pts(cx: float, cy: float,
return pts
DEFAULT_MAX_LAT_ACCEL = 3.0 # m/s^2
class TorqueBar(Widget):
def __init__(self, demo: bool = False, scale: float = 1.0, always: bool = False):
super().__init__()
@@ -167,16 +170,23 @@ class TorqueBar(Widget):
controls_state = ui_state.sm['controlsState']
car_state = ui_state.sm['carState']
live_parameters = ui_state.sm['liveParameters']
lateral_acceleration = controls_state.curvature * car_state.vEgo ** 2 - live_parameters.roll * ACCELERATION_DUE_TO_GRAVITY
# TODO: pull from carparams
max_lateral_acceleration = 3
car_control = ui_state.sm['carControl']
# from selfdrived
# Include lateral accel error in estimated torque utilization
actual_lateral_accel = controls_state.curvature * car_state.vEgo ** 2
desired_lateral_accel = controls_state.desiredCurvature * car_state.vEgo ** 2
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:
self._torque_filter.update(-ui_state.sm['carOutput'].actuatorsOutput.torque)

View File

@@ -68,7 +68,9 @@ def test_dialogs_do_not_leak():
for ctor in (
# mici
MiciDriverCameraDialog, MiciTrainingGuide, MiciOnboardingWindow, MiciPairingDialog,
MiciDriverCameraDialog, MiciPairingDialog,
lambda: MiciTrainingGuide(lambda: None),
lambda: MiciOnboardingWindow(lambda: None),
lambda: BigDialog("test", "test"),
lambda: BigConfirmationDialogV2("test", "icons_mici/settings/network/new/trash.png"),
lambda: BigInputDialog("test"),

View File

@@ -120,6 +120,7 @@ class BigButton(Widget):
self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps)
self._click_delay = 0.075
self._shake_start: float | None = None
self._grow_animation_until: 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_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:
# 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
@@ -182,6 +186,9 @@ class BigButton(Widget):
def trigger_shake(self):
self._shake_start = rl.get_time()
def trigger_grow_animation(self, duration: float = 0.65):
self._grow_animation_until = rl.get_time() + duration
@property
def _shake_offset(self) -> float:
SHAKE_DURATION = 0.5
@@ -197,6 +204,10 @@ class BigButton(Widget):
super().set_position(x + self._shake_offset, y)
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
txt_bg = self._txt_default_bg
if not self.enabled:
@@ -204,7 +215,7 @@ class BigButton(Widget):
elif self.is_pressed:
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_y = self._rect.y + (self._rect.height * (1 - scale)) / 2
return txt_bg, btn_x, btn_y, scale

View File

@@ -103,11 +103,12 @@ class BigInputDialog(BigDialogBase):
hint: str,
default_text: str = "",
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__()
self._hint_label = UnifiedLabel(hint, font_size=35, text_color=rl.Color(255, 255, 255, int(255 * 0.35)),
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_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack + NavWidget
self._minimum_length = minimum_length

View File

@@ -118,7 +118,7 @@ class AlertRenderer(Widget):
alert = self.get_alert(ui_state.sm)
if gui_app.sunnypilot_ui():
ui_state.onroad_brightness_handle_alerts(ui_state.started, alert)
ui_state.onroad_brightness_handle_alerts(ui_state, alert)
if not alert:
return

View File

@@ -91,11 +91,12 @@ class AugmentedRoadView(CameraView, AugmentedRoadViewSP):
# Render the base camera view
super()._render(rect)
if ui_state.hide_camera:
rl.draw_rectangle_rec(self._content_rect, rl.BLACK)
# Draw all UI overlays
self.model_renderer.render(self._content_rect)
AugmentedRoadViewSP.update_fade_out_bottom_overlay(self, self._content_rect)
self.update_confidence_visual(self._content_rect)
self._hud_renderer.render(self._content_rect)
self.alert_renderer.render(self._content_rect)
self.driver_state_renderer.render(self._content_rect)

View File

@@ -12,8 +12,7 @@ from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.sunnypilot.widgets.list_view import option_item_sp, ToggleActionSP
ONROAD_BRIGHTNESS_TIMER_VALUES = {0: 15, 1: 30, **{i: (i - 1) * 60 for i in range(2, 12)}}
from openpilot.sunnypilot.system.params_migration import ONROAD_BRIGHTNESS_TIMER_VALUES
class OnroadBrightness(IntEnum):
@@ -46,7 +45,7 @@ class DisplayLayout(Widget):
title=lambda: tr("Onroad Brightness Delay"),
description="",
min_value=0,
max_value=11,
max_value=15,
value_change_step=1,
value_map=ONROAD_BRIGHTNESS_TIMER_VALUES,
label_callback=lambda value: f"{value} s" if value < 60 else f"{int(value/60)} m",
@@ -92,7 +91,11 @@ class DisplayLayout(Widget):
if isinstance(_item.action_item, ToggleActionSP) and _item.action_item.toggle.param_key is not None:
_item.action_item.set_state(self._params.get_bool(_item.action_item.toggle.param_key))
elif isinstance(_item.action_item, OptionControlSP) and _item.action_item.param_key is not None:
_item.action_item.set_value(self._params.get(_item.action_item.param_key, return_default=True))
raw_value = self._params.get(_item.action_item.param_key, return_default=True)
if _item.action_item.value_map:
reverse_map = {v: k for k, v in _item.action_item.value_map.items()}
raw_value = reverse_map.get(raw_value, _item.action_item.current_value)
_item.action_item.set_value(raw_value)
brightness_val = self._params.get("OnroadScreenOffBrightness", return_default=True)
self._onroad_brightness_timer.action_item.set_enabled(brightness_val not in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK))

View File

@@ -93,6 +93,11 @@ class VisualsLayout(Widget):
"This displays what the car is currently doing, not what the planner is requesting."),
None,
),
"HideCamera": (
lambda: tr("Hide Camera"),
tr("Hide the camera live view from the driving screen."),
None,
),
}
self._toggles = {}
for param, (title, desc, callback) in self._toggle_defs.items():
@@ -120,18 +125,10 @@ class VisualsLayout(Widget):
button_width=350,
inline=False
)
self._confidence_visual = multiple_button_item_sp(
title=lambda: tr("Confidence Visual"),
description=lambda: tr("Display confidence ball or bar on the driving screen."),
buttons=[lambda: tr("Off"), lambda: tr("Ball"), lambda: tr("Bar")],
param="ConfidenceVisual",
inline=False
)
items = list(self._toggles.values()) + [
self._chevron_info,
self._dev_ui_info,
self._confidence_visual,
]
return items
@@ -143,8 +140,6 @@ class VisualsLayout(Widget):
self._dev_ui_info.action_item.set_selected_button(ui_state.params.get("DevUIInfo", return_default=True))
self._confidence_visual.action_item.set_selected_button(ui_state.params.get("ConfidenceVisual", return_default=True))
if ui_state.has_longitudinal_control:
self._chevron_info.set_description(tr(CHEVRON_INFO_DESCRIPTION["enabled"]))
self._chevron_info.action_item.set_selected_button(ui_state.params.get("ChevronInfo", return_default=True))

View File

@@ -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.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.slider import SmallSlider
from openpilot.system.ui.mici_setup import TermsHeader, TermsPage as SetupTermsPage
from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined
from openpilot.selfdrive.ui.ui_state import ui_state
from collections.abc import Callable
from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialogV2
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.mici_setup import GreyBigButton
from openpilot.system.ui.widgets.scroller import NavScroller
class SunnylinkConsentPage(SetupTermsPage):
def __init__(self, on_accept=None, on_decline=None, left_text: str = "disable", right_text: str = "enable"):
super().__init__(on_accept, on_decline, left_text, continue_text=right_text)
class SunnylinkConsentPage(NavScroller):
def __init__(self, on_accept: Callable | None = None, on_decline: Callable | None = None):
super().__init__()
self._title_header = TermsHeader("sunnylink",
gui_app.texture("../../sunnypilot/selfdrive/assets/logo.png", 66, 60))
def show_accept_dialog():
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, " +
"including settings management, remote monitoring, real-time dashboard, etc.",
36, FontWeight.ROMAN)
def show_decline_dialog():
gui_app.push_widget(BigConfirmationDialogV2("disable\nsunnylink", "icons_mici/setup/cancel.png",
red=True, confirm_callback=on_decline))
@property
def _content_height(self):
return self._terms_label.rect.y + self._terms_label.rect.height - self._scroll_panel.get_offset()
self._accept_button = BigCircleButton("icons_mici/setup/driver_monitoring/dm_check.png")
self._accept_button.set_click_callback(show_accept_dialog)
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._decline_button = BigCircleButton("icons_mici/setup/cancel.png", red=True)
self._decline_button.set_click_callback(show_decline_dialog)
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 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)
self._scroller.add_widgets([
GreyBigButton("sunnylink", "scroll to continue",
gui_app.texture("../../sunnypilot/selfdrive/assets/logo.png", 64, 64)),
GreyBigButton("", "sunnylink enables secured remote access to your comma device from anywhere."),
self._accept_button,
self._decline_button,
])

View File

@@ -4,7 +4,7 @@ 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.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.selfdrive.ui.onroad.augmented_road_view import BORDER_COLORS
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
@@ -19,26 +19,8 @@ class ConfidenceBallSP:
@staticmethod
def get_lat_long_dot_color():
from openpilot.selfdrive.ui.onroad.augmented_road_view import BORDER_COLORS
if ui_state.status == UIStatus.LAT_ONLY:
return BORDER_COLORS[UIStatus.LAT_ONLY]
# UIStatus.LONG_ONLY
return BORDER_COLORS[UIStatus.LONG_ONLY]
def update_confidence_visual(self, content_rect, status_dot_radius, dot_height, top_dot_color, bottom_dot_color) -> bool:
if self._visual == 2:
bar_width = int(20 * self._scale)
bar_x = content_rect.x + content_rect.width - bar_width
fill_h = int(content_rect.height * self._confidence_filter.x)
fill_y = int(content_rect.y + (content_rect.height - fill_h))
rl.draw_rectangle(int(bar_x), int(content_rect.y), bar_width, int(content_rect.height), rl.Color(20, 20, 20, 180))
rl.draw_rectangle_gradient_v(int(bar_x), fill_y, bar_width, fill_h, top_dot_color, bottom_dot_color)
return True
elif self._visual == 1:
rl.draw_circle_gradient(int(content_rect.x + content_rect.width - status_dot_radius),
int(dot_height), status_dot_radius,
top_dot_color, bottom_dot_color)
return True
return False

View File

@@ -8,7 +8,6 @@ import pyray as rl
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.selfdrive.ui.ui_state import UIStatus, ui_state
from openpilot.system.ui.lib.application import gui_app
from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall
BORDER_COLORS_SP = {
UIStatus.LAT_ONLY: rl.Color(0x00, 0xC8, 0xC8, 0xFF), # Cyan for lateral-only state
@@ -20,20 +19,13 @@ class AugmentedRoadViewSP:
def __init__(self):
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
self._fade_alpha_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps)
self._confidence_visual = ConfidenceBall(scale=1.5)
def update_fade_out_bottom_overlay(self, _content_rect):
# Fade out bottom of overlays for looks (only when engaged)
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
rl.draw_texture_pro(self._fade_texture,
rl.Rectangle(0, 0, self._fade_texture.width, self._fade_texture.height),
_content_rect, rl.Vector2(0, 0), 0.0,
rl.Color(255, 255, 255, int(255 * fade_alpha)))
def update_confidence_visual(self, _content_rect):
mode = ui_state.confidence_visual
if mode in (1, 2):
self._confidence_visual._visual = mode
self._confidence_visual.render(_content_rect)

View File

@@ -131,7 +131,7 @@ class HudRendererSP(HudRenderer):
def _render(self, rect: rl.Rectangle) -> None:
super()._render(rect)
if ui_state.torque_bar and ui_state.sm['controlsState'].lateralControlState.which() != 'angleState':
if ui_state.torque_bar:
torque_rect = rect
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())

View File

@@ -49,8 +49,11 @@ class UIStateSP:
else:
self.sunnylink_state.stop()
def onroad_brightness_handle_alerts(self, started: bool, alert):
has_alert = started and self.onroad_brightness != OnroadBrightness.AUTO and alert is not None
def onroad_brightness_handle_alerts(self, _ui_state, alert):
if _ui_state.sm.recv_frame["carState"] < _ui_state.started_frame:
return
has_alert = _ui_state.started and self.onroad_brightness != OnroadBrightness.AUTO and alert is not None
self.update_onroad_brightness(has_alert)
if has_alert:
@@ -130,6 +133,7 @@ class UIStateSP:
self.chevron_metrics = self.params.get("ChevronInfo")
self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True)
self.developer_ui = self.params.get("DevUIInfo")
self.hide_camera = self.params.get_bool("HideCamera")
self.hide_v_ego_ui = self.params.get_bool("HideVEgoUI")
self.onroad_brightness = int(float(self.params.get("OnroadScreenOffBrightness", return_default=True)))
self.onroad_brightness_timer_param = self.params.get("OnroadScreenOffTimer", return_default=True)
@@ -143,7 +147,6 @@ class UIStateSP:
self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI")
self.turn_signals = self.params.get_bool("ShowTurnSignals")
self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True)
self.confidence_visual = self.params.get("ConfidenceVisual", return_default=True)
class DeviceSP:

View File

@@ -116,7 +116,7 @@ class ModelCache:
class ModelFetcher:
"""Handles fetching and caching of model data from remote source"""
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v15.json"
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v15.json"
def __init__(self, params: Params):
self.params = params

View File

@@ -72,9 +72,10 @@ def _flash_panda(panda: Panda) -> None:
_flash_static(panda._handle, code)
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):
cloudlog.error(f"Rivian longitudinal upgrade firmware not found at {FW_PATH}")
return
@@ -83,13 +84,22 @@ def flash_rivian_long(panda: Panda) -> None:
cloudlog.info("Not a Rivian, skipping longitudinal upgrade...")
return
# only flash external black pandas (HW_TYPE_BLACK = 0x03)
if panda.get_type() == b'\x03' and not panda.is_internal():
try:
_flash_panda(panda)
except Exception:
cloudlog.exception(f"Failed to flash F4 panda {panda.get_usb_serial()}")
# only check USB connected pandas, internal panda uses SPI and is never an external panda
usb_serials = set(Panda.usb_list())
for serial in panda_serials:
if serial not in usb_serials:
continue
panda = Panda(serial)
# only flash external black pandas (HW_TYPE_BLACK = 0x03)
if panda.get_type() == b'\x03' and not panda.is_internal():
try:
_flash_panda(panda)
except Exception:
cloudlog.exception(f"Failed to flash xnor's Rivian Longitudinal Upgrade Kit: {serial}")
panda.close()
return
if __name__ == '__main__':
flash_rivian_long(Panda())
flash_rivian_long(Panda.list())

View File

@@ -186,24 +186,6 @@
"title": "Completed Training Version",
"description": ""
},
"ConfidenceVisual": {
"title": "Confidence Visual",
"description": "Display confidence ball or bar on the driving screen.",
"options": [
{
"value": 0,
"label": "Off"
},
{
"value": 1,
"label": "Ball"
},
{
"value": 2,
"label": "Bar"
}
]
},
"ControlsReady": {
"title": "Controls Ready",
"description": ""
@@ -388,6 +370,10 @@
"title": "Has Accepted sunnypilot Terms",
"description": ""
},
"HideCamera": {
"title": "Hide Camera",
"description": "Hide the camera live view from the driving screen."
},
"HideVEgoUI": {
"title": "[TIZI/TICI only] Speedometer: Hide from Onroad Screen",
"description": "When enabled, the speedometer on the onroad screen is not displayed."
@@ -959,6 +945,22 @@
"title": "Onroad Brightness Delay",
"description": "",
"options": [
{
"value": 3,
"label": "3s"
},
{
"value": 5,
"label": "5s"
},
{
"value": 7,
"label": "7s"
},
{
"value": 10,
"label": "10s"
},
{
"value": 15,
"label": "15s"
@@ -1009,6 +1011,10 @@
}
]
},
"OnroadScreenOffTimerMigrated": {
"title": "Onroad Brightness Delay Migration Version",
"description": "This param is to track whether OnroadScreenOffTimer needs to be migrated."
},
"OnroadUploads": {
"title": "Onroad Uploads",
"description": ""
@@ -1308,7 +1314,7 @@
"min": 0.1,
"max": 5.0,
"step": 0.1,
"unit": "m/s²"
"unit": "m/s\u00b2"
},
"ToyotaEnforceStockLongitudinal": {
"title": "Toyota: Enforce Factory Longitudinal Control",

View File

@@ -10,6 +10,7 @@ import os
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.sunnypilot.system.params_migration import ONROAD_BRIGHTNESS_TIMER_VALUES
METADATA_PATH = os.path.join(os.path.dirname(__file__), "../params_metadata.json")
TORQUE_VERSIONS_JSON = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "controls", "lib", "latcontrol_torque_versions.json")
@@ -56,6 +57,9 @@ def main():
# update onroad screen brightness params
update_onroad_brightness_param()
# update onroad screen brightness timer params
update_onroad_brightness_timer_param()
# update torque versions param
update_torque_versions_param()
@@ -81,6 +85,24 @@ def update_onroad_brightness_param():
print(f"Failed to update OnroadScreenOffBrightness versions in params_metadata.json: {e}")
def update_onroad_brightness_timer_param():
try:
with open(METADATA_PATH) as f:
params_metadata = json.load(f)
if "OnroadScreenOffTimer" in params_metadata:
options = []
for _index, seconds in sorted(ONROAD_BRIGHTNESS_TIMER_VALUES.items()):
label = f"{seconds}s" if seconds < 60 else f"{seconds // 60}m"
options.append({"value": seconds, "label": label})
params_metadata["OnroadScreenOffTimer"]["options"] = options
with open(METADATA_PATH, 'w') as f:
json.dump(params_metadata, f, indent=2)
f.write('\n')
print(f"Updated OnroadScreenOffTimer options in params_metadata.json with {len(options)} options.")
except Exception as e:
print(f"Failed to update OnroadScreenOffTimer options in params_metadata.json: {e}")
def update_torque_versions_param():
with open(TORQUE_VERSIONS_JSON) as f:
current_versions = json.load(f)

View File

@@ -7,13 +7,18 @@ See the LICENSE.md file in the root directory for more details.
from openpilot.common.swaglog import cloudlog
ONROAD_BRIGHTNESS_MIGRATION_VERSION: str = "1.0"
ONROAD_BRIGHTNESS_TIMER_MIGRATION_VERSION: str = "1.0"
# index → seconds mapping for OnroadScreenOffTimer (SSoT)
ONROAD_BRIGHTNESS_TIMER_VALUES = {0: 3, 1: 5, 2: 7, 3: 10, 4: 15, 5: 30, **{i: (i - 5) * 60 for i in range(6, 16)}}
VALID_TIMER_VALUES = set(ONROAD_BRIGHTNESS_TIMER_VALUES.values())
def run_migration(_params):
# migrate OnroadScreenOffBrightness
if _params.get("OnroadScreenOffBrightnessMigrated") != ONROAD_BRIGHTNESS_MIGRATION_VERSION:
try:
val = _params.get("OnroadScreenOffBrightness")
val = _params.get("OnroadScreenOffBrightness", return_default=True)
if val >= 2: # old: 5%, new: Screen Off
new_val = val + 1
_params.put("OnroadScreenOffBrightness", new_val)
@@ -25,3 +30,18 @@ def run_migration(_params):
cloudlog.info(log_str + f" Setting OnroadScreenOffBrightnessMigrated to {ONROAD_BRIGHTNESS_MIGRATION_VERSION}")
except Exception as e:
cloudlog.exception(f"Error migrating OnroadScreenOffBrightness: {e}")
# migrate OnroadScreenOffTimer
if _params.get("OnroadScreenOffTimerMigrated") != ONROAD_BRIGHTNESS_TIMER_MIGRATION_VERSION:
try:
val = _params.get("OnroadScreenOffTimer", return_default=True)
if val not in VALID_TIMER_VALUES:
_params.put("OnroadScreenOffTimer", 15)
log_str = f"Successfully migrated OnroadScreenOffTimer from {val} to 15 (default)."
else:
log_str = "Migration not required for OnroadScreenOffTimer."
_params.put("OnroadScreenOffTimerMigrated", ONROAD_BRIGHTNESS_TIMER_MIGRATION_VERSION)
cloudlog.info(log_str + f" Setting OnroadScreenOffTimerMigrated to {ONROAD_BRIGHTNESS_TIMER_MIGRATION_VERSION}")
except Exception as e:
cloudlog.exception(f"Error migrating OnroadScreenOffTimer: {e}")

Binary file not shown.

View File

@@ -51,7 +51,8 @@ def manager_init() -> None:
if params.get_bool("RecordFrontLock"):
params.put_bool("RecordFront", True)
run_migration(params)
if not PC:
run_migration(params)
# set unset params to their default value
for k in params.all_keys():

View File

@@ -436,6 +436,9 @@ class GuiApplication(GuiApplicationExt):
return self._nav_stack[-1]
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]):
if tick_function not in self._nav_stack_ticks:
self._nav_stack_ticks.append(tick_function)

View File

@@ -1,12 +1,13 @@
import io
import re
import functools
from importlib.resources import as_file
from PIL import Image, ImageDraw, ImageFont
import pyray as rl
from openpilot.system.ui.lib.application import FONT_DIR
_emoji_font: ImageFont.FreeTypeFont | None = None
_cache: dict[str, rl.Texture] = {}
EMOJI_REGEX = re.compile(
@@ -33,11 +34,10 @@ EMOJI_REGEX = re.compile(
flags=re.UNICODE
)
def _load_emoji_font() -> ImageFont.FreeTypeFont | None:
global _emoji_font
if _emoji_font is None:
_emoji_font = ImageFont.truetype(str(FONT_DIR.joinpath("NotoColorEmoji.ttf")), 109)
return _emoji_font
@functools.cache
def _load_emoji_font() -> ImageFont.FreeTypeFont:
with as_file(FONT_DIR.joinpath("NotoColorEmoji.ttf")) as font_path:
return ImageFont.truetype(io.BytesIO(font_path.read_bytes()), 109)
def find_emoji(text):
return [(m.start(), m.end(), m.group()) for m in EMOJI_REGEX.finditer(text)]

View File

@@ -74,7 +74,7 @@ def load_translations(path) -> tuple[dict[str, str], dict[str, list[str]]]:
translations: msgid -> msgstr
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()
translations: dict[str, str] = {}

View File

@@ -91,6 +91,7 @@ class Reset(Widget):
if self._mode == ResetMode.RECOVER:
self._cancel_button.set_text("reboot")
self._cancel_button.set_click_callback(self._do_reboot)
self._cancel_button.render(rl.Rectangle(
rect.x + 8,
rect.y + rect.height - self._cancel_button.rect.height,

View File

@@ -1,5 +1,4 @@
#!/usr/bin/env python3
from abc import abstractmethod
import os
import re
import threading
@@ -14,21 +13,22 @@ import pyray as rl
from cereal import log
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.swaglog import cloudlog
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.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.nav_widget import NavWidget
from openpilot.system.ui.widgets.button import (IconButton, SmallButton, WideRoundedButton, SmallerRoundedButton,
SmallCircleIconButton, WidishRoundedButton, FullRoundedButton)
from openpilot.system.ui.widgets.button import SmallButton
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.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.button import BigButton
NetworkType = log.DeviceState.NetworkType
@@ -122,9 +122,9 @@ class SoftwareSelectionPage(NavWidget):
use_custom_software_callback: Callable):
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._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)
def show_event(self):
@@ -161,199 +161,24 @@ class SoftwareSelectionPage(NavWidget):
self._custom_software_slider.render(custom_software_rect)
class TermsHeader(Widget):
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):
class CustomSoftwareWarningPage(NavScroller):
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",
gui_app.texture("icons_mici/setup/warning.png", 66, 60))
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._continue_button = BigPillButton("next")
self._continue_button.set_click_callback(continue_callback)
self._restore_header = TermsHeader("how to backup &\nrestore", gui_app.texture("icons_mici/setup/restore.png", 60, 60))
self._restore_body = UnifiedLabel("To restore your device to a factory state later, use https://flash.comma.ai",
36, text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
font_weight=FontWeight.ROMAN)
@property
def _content_height(self):
return self._restore_body.rect.y + self._restore_body.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 + 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)),
))
self._scroller.add_widgets([
GreyBigButton("use caution", "when installing\n3rd party software",
gui_app.texture("icons_mici/setup/warning.png", 64, 58)),
GreyBigButton("", "• It has not been tested by comma"),
GreyBigButton("", "• It may not comply with relevant safety standards."),
GreyBigButton("", "• It may cause damage to your device and/or vehicle."),
GreyBigButton("how to restore to a\nfactory state later", "https://flash.comma.ai",
gui_app.texture("icons_mici/setup/restore.png", 64, 64)),
self._continue_button,
])
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"):
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)),
font_weight=FontWeight.DISPLAY)
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],
back_callback: Callable[[], None] | None):
disable_connect_hint: bool = False):
super().__init__()
self.set_back_callback(back_callback)
self._wifi_manager = WifiManager()
self._wifi_manager.set_active(True)
@@ -459,83 +357,106 @@ class NetworkSetupPage(NavWidget):
self._prev_has_internet = False
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._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 58, 50)
self._waiting_text = "waiting for internet..."
self._network_header = TermsHeader(self._waiting_text, self._no_wifi_txt)
self._connect_button = GreyBigButton("connect to\ninternet", "swipe down to go back",
gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 64, 56, flip_x=True))
self._connect_button.set_visible(not disable_connect_hint)
back_txt = gui_app.texture("icons_mici/setup/back_new.png", 37, 32)
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 = WifiNetworkButton(self._wifi_manager)
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._continue_button.set_enabled(False)
self._show_time = 0.0
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._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)
def show_event(self):
super().show_event()
self._show_time = rl.get_time()
self._prev_has_internet = False
self._network_monitor.reset()
self._set_has_internet(False)
self._pending_has_internet_scroll = False
self._pending_continue_grow_animation = False
self._pending_wifi_grow_animation = False
def _nav_stack_tick(self):
self._wifi_manager.process_callbacks()
has_internet = self._network_monitor.network_connected.is_set()
if has_internet != self._prev_has_internet:
self._set_has_internet(has_internet)
if has_internet:
gui_app.pop_widgets_to(self)
self._prev_has_internet = has_internet
if has_internet and not self._prev_has_internet:
self._pending_has_internet_scroll = True
self._prev_has_internet = has_internet
def _set_has_internet(self, has_internet: bool):
if has_internet:
self._network_header.set_title("connected to internet")
self._network_header.set_icon(self._wifi_full_txt)
self._continue_button.set_enabled(lambda: self.enabled)
else:
self._network_header.set_title(self._waiting_text)
self._network_header.set_icon(self._no_wifi_txt)
self._continue_button.set_enabled(False)
if self._pending_has_internet_scroll:
# Scrolls over to continue button, then grows once in view
elapsed = rl.get_time() - self._show_time
if elapsed > 0.5:
self._pending_has_internet_scroll = False
def scroll_to_download():
self._scroller._layout()
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):
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, _):
self._network_header.render(rl.Rectangle(
self._rect.x + 16,
self._rect.y + 16,
self._rect.width - 32,
self._network_header.rect.height,
))
def set_is_updater(self):
self._continue_button.set_text("download\n& install")
self._continue_button.set_green(False)
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,
))
def _update_state(self):
super()._update_state()
self._wifi_button.render(rl.Rectangle(
self._rect.x + 8 + self._back_button.rect.width + 10,
self._rect.y + self._rect.height - self._wifi_button.rect.height,
self._wifi_button.rect.width,
self._wifi_button.rect.height,
))
if self._pending_continue_grow_animation:
btn_right = self._continue_button.rect.x + self._continue_button.rect.width
visible_right = self._rect.x + self._rect.width
if btn_right < visible_right + 50:
self._pending_continue_grow_animation = False
self._continue_button.trigger_grow_animation()
self._continue_button.render(rl.Rectangle(
self._rect.x + self._rect.width - self._continue_button.rect.width - 8,
self._rect.y + self._rect.height - self._continue_button.rect.height,
self._continue_button.rect.width,
self._continue_button.rect.height,
))
if self._pending_wifi_grow_animation and abs(self._wifi_button.rect.x - ITEM_SPACING) < 50:
self._pending_wifi_grow_animation = False
self._wifi_button.trigger_grow_animation()
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):
@@ -557,13 +478,13 @@ class Setup(Widget):
self._start_page.set_click_callback(getting_started_button_callback)
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._pop_to_software_selection)
self._network_setup_page = NetworkSetupPage(self._network_monitor, self._network_setup_continue_callback, self._pop_to_software_selection)
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._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()
@@ -602,17 +523,14 @@ class Setup(Widget):
time.sleep(0.1)
gui_app.request_close()
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)
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):
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):
def _network_setup_continue_callback(self, custom_software: bool):
if not custom_software:
gui_app.pop_widgets_to(self._software_selection_page, instant=True) # don't reset sliders
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
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)
def _download(self, url: str):

View File

@@ -5,13 +5,14 @@ import threading
import pyray as rl
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.wifi_manager import WifiManager
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel
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):
@@ -32,16 +33,15 @@ class Updater(Widget):
self.progress_text = "loading"
self.process = 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.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
self._continue_button = FullRoundedButton("continue")
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)),
font_weight=FontWeight.ROMAN)
self._update_failed_page = FailedPage(HARDWARE.reboot, self._update_failed_retry_callback,
title="update failed")
self._update_failed_page = FailedPageBase(HARDWARE.reboot, self._update_failed_retry_callback,
title="update failed")
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)
@@ -61,10 +61,7 @@ class Updater(Widget):
font_weight=FontWeight.ROMAN,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
def _network_setup_back_callback(self):
self.set_current_screen(Screen.PROMPT)
def _network_setup_continue_callback(self):
def _network_setup_continue_callback(self, _):
self.install_update()
def _update_failed_retry_callback(self):
@@ -99,9 +96,13 @@ class Updater(Widget):
def _run_update_process(self):
# TODO: just import it and run in a thread without a subprocess
cmd = [self.updater, "--swap", self.manifest]
self.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, bufsize=1, universal_newlines=True)
try:
cmd = [self.updater, "--swap", self.manifest]
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:
for line in self.process.stdout:
@@ -160,14 +161,10 @@ class Updater(Widget):
rect.height,
))
def _update_state(self):
self._wifi_manager.process_callbacks()
def _render(self, rect: rl.Rectangle):
if self.current_screen == Screen.PROMPT:
self.render_prompt_screen(rect)
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)
elif self.current_screen == Screen.PROGRESS:
self.render_progress_screen(rect)
@@ -179,6 +176,14 @@ class Updater(Widget):
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:
print("Usage: updater.py <updater_path> <manifest_path>")
sys.exit(1)

View File

@@ -67,9 +67,14 @@ class Updater(Widget):
def _run_update_process(self):
# TODO: just import it and run in a thread without a subprocess
cmd = [self.updater, "--swap", self.manifest]
self.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, bufsize=1, universal_newlines=True)
try:
cmd = [self.updater, "--swap", self.manifest]
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:
for line in self.process.stdout:

View File

@@ -228,6 +228,7 @@ class SmallCircleIconButton(Widget):
class SmallButton(Widget):
def __init__(self, text: str):
super().__init__()
self._click_delay = 0.075
self._opacity_filter = FirstOrderFilter(1.0, 0.1, 1 / gui_app.target_fps)
self._load_assets()

View File

@@ -146,8 +146,9 @@ class CapsState(IntEnum):
class MiciKeyboard(Widget):
def __init__(self):
def __init__(self, auto_return_to_letters: str = ""):
super().__init__()
self._auto_return_to_letters = auto_return_to_letters
lower_chars = [
"qwertyuiop",
@@ -305,6 +306,10 @@ class MiciKeyboard(Widget):
if self._caps_state == CapsState.UPPER:
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
key_selected_dt = rl.get_time() - (self._selected_key_t or 0)
cur_t = rl.get_time()

View File

@@ -63,7 +63,7 @@ class NavWidget(Widget, abc.ABC):
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._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
# 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:
gui_app.pop_widget()
if self._back_callback is not None:
self._back_callback()
# Only one callback should ever be fired
if self._dismiss_callback is not None:
self._dismiss_callback()
self._dismiss_callback = None
elif self._back_callback is not None:
self._back_callback()
self._playing_dismiss_animation = False
self._drag_start_pos = None

View File

@@ -13,7 +13,6 @@ if arch == "Darwin":
has_qt = False
else:
has_qt = shutil.which('qmake') is not None
if not has_qt:
Return()
@@ -21,7 +20,7 @@ SConscript(['#tools/replay/SConscript'])
Import('replay_lib')
qt_env = env.Clone()
qt_modules = ["Widgets", "Gui", "Core", "Network", "Concurrent", "DBus", "Xml"]
qt_modules = ["Widgets", "Gui", "Core"]
qt_libs = []
if arch == "Darwin":
@@ -51,7 +50,7 @@ else:
qt_env['QT3DIR'] = qt_env['QTDIR']
qt_env.Tool('qt3')
qt_env['CPPPATH'] += qt_dirs + ["#third_party/qrcode"]
qt_env['CPPPATH'] += qt_dirs
qt_flags = [
"-D_REENTRANT",
"-DQT_NO_DEBUG",
@@ -69,10 +68,8 @@ base_libs = [common, messaging, cereal, visionipc, 'm', 'ssl', 'crypto', 'pthrea
if arch == "Darwin":
base_frameworks.append('QtCharts')
base_frameworks.append('QtSerialBus')
else:
base_libs.append('Qt5Charts')
base_libs.append('Qt5SerialBus')
qt_libs = base_libs

View File

@@ -111,7 +111,8 @@ void BinaryView::highlight(const cabana::Signal *sig) {
if (sig != hovered_sig) {
for (int i = 0; i < model->items.size(); ++i) {
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());
emit model->dataChanged(index, index, {Qt::DisplayRole});
}
@@ -157,7 +158,7 @@ void BinaryView::mousePressEvent(QMouseEvent *event) {
void BinaryView::highlightPosition(const QPoint &pos) {
if (auto index = indexAt(viewport()->mapFromGlobal(pos)); index.isValid()) {
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);
}
}
@@ -208,12 +209,12 @@ void BinaryView::refresh() {
highlightPosition(QCursor::pos());
}
QSet<const cabana::Signal *> BinaryView::getOverlappingSignals() const {
QSet<const cabana::Signal *> overlapping;
std::set<const cabana::Signal *> BinaryView::getOverlappingSignals() const {
std::set<const cabana::Signal *> overlapping;
for (const auto &item : model->items) {
if (item.sigs.size() > 1) {
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 idx = column_count * (pos / 8) + pos % 8;
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;
}
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;
auto model = (const BinaryViewModel*)(index.model());
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 {
@@ -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);
painter->fillRect(option.rect, color);
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) {
for (auto &s : item->sigs) {
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) {
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));
}

View File

@@ -1,10 +1,9 @@
#pragma once
#include <set>
#include <tuple>
#include <vector>
#include <QList>
#include <QSet>
#include <QStyledItemDelegate>
#include <QTableView>
@@ -51,7 +50,7 @@ public:
bool is_msb = false;
bool is_lsb = false;
uint8_t val;
QList<const cabana::Signal *> sigs;
std::vector<const cabana::Signal *> sigs;
bool valid = false;
};
std::vector<Item> items;
@@ -68,7 +67,7 @@ public:
BinaryView(QWidget *parent = nullptr);
void setMessage(const MessageId &message_id);
void highlight(const cabana::Signal *sig);
QSet<const cabana::Signal*> getOverlappingSignals() const;
std::set<const cabana::Signal*> getOverlappingSignals() const;
void updateState() { model->updateState(); }
void paintEvent(QPaintEvent *event) override {
is_message_active = can->isMessageActive(model->msg_id);

View File

@@ -46,13 +46,13 @@ int main(int argc, char *argv[]) {
stream = new DeviceStream(&app, cmd_parser.value("zmq"));
} else if (cmd_parser.isSet("panda") || cmd_parser.isSet("panda-serial")) {
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) {
qWarning() << e.what();
return 0;
}
} 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 {
uint32_t replay_flags = REPLAY_FLAG_NONE;
if (cmd_parser.isSet("ecam")) replay_flags |= REPLAY_FLAG_ECAM;
@@ -70,7 +70,7 @@ int main(int argc, char *argv[]) {
if (!route.isEmpty()) {
auto replay_stream = std::make_unique<ReplayStream>(&app);
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;
}
stream = replay_stream.release();

View File

@@ -237,8 +237,8 @@ void ChartView::updateTitle() {
for (auto &s : sigs) {
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>")
.arg(decoration, titleColorCss, s.sig->name,
msgColorCss, msgName(s.msg_id), s.msg_id.toString()));
.arg(decoration, titleColorCss, QString::fromStdString(s.sig->name),
msgColorCss, QString::fromStdString(msgName(s.msg_id)), QString::fromStdString(s.msg_id.toString())));
}
split_chart_act->setEnabled(sigs.size() > 1);
resetChartCache();
@@ -339,13 +339,13 @@ void ChartView::updateAxisY() {
double min = std::numeric_limits<double>::max();
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) {
if (!s.series->isVisible()) continue;
// Only show unit when all signals have the same unit
if (unit != s.sig->unit) {
if (unit != QString::fromStdString(s.sig->unit)) {
unit.clear();
}
@@ -573,11 +573,11 @@ void ChartView::showTip(double 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; });
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;
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 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)")
@@ -766,7 +766,7 @@ void ChartView::drawSignalValue(QPainter *painter) {
for (auto &s : sigs) {
auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), cur_sec,
[](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 value_rect(marker_rect.bottomLeft() - QPoint(0, 1), marker_rect.size());
QString elided_val = painter->fontMetrics().elidedText(value, Qt::ElideRight, value_rect.width());

View File

@@ -1,13 +1,13 @@
#include "tools/cabana/chart/chartswidget.h"
#include <algorithm>
#include <future>
#include <QApplication>
#include <QFutureSynchronizer>
#include <QMenu>
#include <QMimeData>
#include <QScrollBar>
#include <QToolBar>
#include <QtConcurrent>
#include "tools/cabana/chart/chart.h"
@@ -166,15 +166,16 @@ void ChartsWidget::removeTab(int index) {
void ChartsWidget::updateTabBar() {
for (int i = 0; i < tabbar->count(); ++i) {
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) {
QFutureSynchronizer<void> future_synchronizer;
std::vector<std::future<void>> futures;
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) {
@@ -203,7 +204,7 @@ void ChartsWidget::showValueTip(double sec) {
}
void ChartsWidget::updateState() {
if (charts.isEmpty()) return;
if (charts.empty()) return;
const auto &time_range = can->timeRange();
const double cur_sec = can->currentSec();
@@ -247,7 +248,7 @@ void ChartsWidget::updateToolBar() {
redo_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) : "");
remove_all_btn->setEnabled(!charts.isEmpty());
remove_all_btn->setEnabled(!charts.empty());
}
void ChartsWidget::settingChanged() {
@@ -281,9 +282,9 @@ ChartView *ChartsWidget::createChart(int pos) {
chart->setMinimumWidth(CHART_MIN_WIDTH);
chart->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
QObject::connect(chart, &ChartView::axisYLabelWidthChanged, align_timer, qOverload<>(&QTimer::start));
pos = std::clamp(pos, 0, charts.size());
charts.insert(pos, chart);
currentCharts().insert(pos, chart);
pos = std::clamp(pos, 0, (int)charts.size());
charts.insert(charts.begin() + pos, chart);
currentCharts().insert(currentCharts().begin() + pos, chart);
updateLayout(true);
updateToolBar();
return chart;
@@ -302,7 +303,7 @@ void ChartsWidget::showChart(const MessageId &id, const cabana::Signal *sig, boo
void ChartsWidget::splitChart(ChartView *src_chart) {
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(); /**/) {
auto c = createChart(pos);
src_chart->chart()->removeSeries(it->series);
@@ -327,7 +328,7 @@ QStringList ChartsWidget::serializeChartIds() const {
for (auto c : charts) {
QStringList ids;
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(',');
}
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(',')) {
const auto sig_parts = part.split('|');
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* sig = msg->sig(sig_parts[1]))
if (auto* sig = msg->sig(sig_parts[1].toStdString()))
showChart(msg_id, sig, true, index++ > 0);
}
}
@@ -426,14 +427,14 @@ void ChartsWidget::doAutoScroll() {
}
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() {
SignalSelector dlg(tr("New Chart"), this);
if (dlg.exec() == QDialog::Accepted) {
auto items = dlg.seletedItems();
if (!items.isEmpty()) {
if (!items.empty()) {
auto c = createChart();
for (auto it : items) {
c->addSignal(it->msg_id, it->sig);
@@ -443,10 +444,10 @@ void ChartsWidget::newChart() {
}
void ChartsWidget::removeChart(ChartView *chart) {
charts.removeOne(chart);
charts.erase(std::remove(charts.begin(), charts.end(), chart), charts.end());
chart->deleteLater();
for (auto &[_, list] : tab_charts) {
list.removeOne(chart);
list.erase(std::remove(list.begin(), list.end(), chart), list.end());
}
updateToolBar();
updateLayout(true);
@@ -460,7 +461,7 @@ void ChartsWidget::removeAll() {
}
tab_charts.clear();
if (!charts.isEmpty()) {
if (!charts.empty()) {
for (auto c : charts) {
delete c;
}
@@ -560,10 +561,11 @@ void ChartsContainer::dropEvent(QDropEvent *event) {
auto chart = qobject_cast<ChartView *>(event->source());
if (w != chart) {
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;
charts_widget->currentCharts().insert(to, chart);
auto &cur = charts_widget->currentCharts();
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->updateTabBar();
event->acceptProposedAction();

View File

@@ -81,7 +81,7 @@ private:
bool eventFilter(QObject *obj, QEvent *event) override;
void newTab();
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);
QLabel *title_label;
@@ -100,8 +100,8 @@ private:
QUndoStack *zoom_undo_stack;
ToolButton *remove_all_btn;
QList<ChartView *> charts;
std::unordered_map<int, QList<ChartView *>> tab_charts;
std::vector<ChartView *> charts;
std::unordered_map<int, std::vector<ChartView *>> tab_charts;
TabBar *tabbar;
ChartsContainer *charts_container;
QScrollArea *charts_scroll;

View File

@@ -46,7 +46,7 @@ SignalSelector::SignalSelector(QString title, QWidget *parent) : QDialog(parent)
for (const auto &[id, _] : can->lastMessages()) {
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);
@@ -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) {
QString text = QString("<span style=\"color:%0;\">■ </span> %1").arg(sig->color.name(), sig->name);
if (show_msg_name) text += QString(" <font color=\"gray\">%0 %1</font>").arg(msgName(id), id.toString());
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(QString::fromStdString(msgName(id)), QString::fromStdString(id.toString()));
QLabel *label = new QLabel(text);
label->setContentsMargins(5, 0, 5, 0);
@@ -102,8 +102,8 @@ void SignalSelector::addItemToList(QListWidget *parent, const MessageId id, cons
parent->setItemWidget(new_item, label);
}
QList<SignalSelector::ListItem *> SignalSelector::seletedItems() {
QList<SignalSelector::ListItem *> ret;
std::vector<SignalSelector::ListItem *> SignalSelector::seletedItems() {
std::vector<SignalSelector::ListItem *> ret;
for (int i = 0; i < selected_list->count(); ++i) ret.push_back((ListItem *)selected_list->item(i));
return ret;
}

View File

@@ -15,7 +15,7 @@ public:
};
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); }
private:

View File

@@ -4,22 +4,22 @@
// EditMsgCommand
EditMsgCommand::EditMsgCommand(const MessageId &id, const QString &name, int size,
const QString &node, const QString &comment, QUndoCommand *parent)
EditMsgCommand::EditMsgCommand(const MessageId &id, const std::string &name, int size,
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) {
if (auto msg = dbc()->msg(id)) {
old_name = msg->name;
old_size = msg->size;
old_node = msg->transmitter;
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 {
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() {
if (old_name.isEmpty())
if (old_name.empty())
dbc()->removeMsg(id);
else
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) {
if (auto msg = dbc()->msg(id)) {
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() {
if (!message.name.isEmpty()) {
if (!message.name.empty()) {
dbc()->updateMsg(id, message.name, message.size, message.transmitter, message.comment);
for (auto s : message.getSignals())
dbc()->addSignal(id, *s);
@@ -47,7 +47,7 @@ void RemoveMsgCommand::undo() {
}
void RemoveMsgCommand::redo() {
if (!message.name.isEmpty())
if (!message.name.empty())
dbc()->removeMsg(id);
}
@@ -55,7 +55,7 @@ void RemoveMsgCommand::redo() {
AddSigCommand::AddSigCommand(const MessageId &id, const cabana::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() {
@@ -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); }
@@ -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); }

View File

@@ -1,6 +1,8 @@
#pragma once
#include <string>
#include <utility>
#include <vector>
#include <QUndoCommand>
#include <QUndoStack>
@@ -10,14 +12,14 @@
class EditMsgCommand : public QUndoCommand {
public:
EditMsgCommand(const MessageId &id, const QString &name, int size, const QString &node,
const QString &comment, QUndoCommand *parent = nullptr);
EditMsgCommand(const MessageId &id, const std::string &name, int size, const std::string &node,
const std::string &comment, QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
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;
};
@@ -52,7 +54,7 @@ public:
private:
const MessageId id;
QList<cabana::Signal> sigs;
std::vector<cabana::Signal> sigs;
};
class EditSignalCommand : public QUndoCommand {
@@ -63,7 +65,7 @@ public:
private:
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 {

View File

@@ -4,10 +4,6 @@
#include "tools/cabana/utils/util.h"
uint qHash(const MessageId &item) {
return qHash(item.source) ^ qHash(item.address);
}
// cabana::Msg
cabana::Msg::~Msg() {
@@ -22,7 +18,7 @@ cabana::Signal *cabana::Msg::addSignal(const cabana::Signal &sig) {
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);
if (s) {
*s = new_sig;
@@ -31,7 +27,7 @@ cabana::Signal *cabana::Msg::updateSignal(const QString &sig_name, const cabana:
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; });
if (it != sigs.end()) {
delete *it;
@@ -57,7 +53,7 @@ cabana::Msg &cabana::Msg::operator=(const cabana::Msg &other) {
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; });
return it != sigs.end() ? *it : nullptr;
}
@@ -69,17 +65,17 @@ int cabana::Msg::indexOf(const cabana::Signal *sig) const {
return -1;
}
QString cabana::Msg::newSignalName() {
QString new_name;
std::string cabana::Msg::newSignalName() {
std::string new_name;
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;
}
return new_name;
}
void cabana::Msg::update() {
if (transmitter.isEmpty()) {
if (transmitter.empty()) {
transmitter = DEFAULT_NODE_NAME;
}
mask.assign(size, 0x00);
@@ -129,13 +125,13 @@ void cabana::Msg::update() {
void cabana::Signal::update() {
updateMsbLsb(*this);
if (receiver_name.isEmpty()) {
if (receiver_name.empty()) {
receiver_name = DEFAULT_NODE_NAME;
}
float h = 19 * (float)lsb / 64.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 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));
}
QString cabana::Signal::formatValue(double value, bool with_unit) const {
std::string cabana::Signal::formatValue(double value, bool with_unit) const {
// Show enum string
int64_t raw_value = round((value - offset) / factor);
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);
if (with_unit && !unit.isEmpty()) {
char buf[64];
snprintf(buf, sizeof(buf), "%.*f", precision, value);
std::string val_str(buf);
if (with_unit && !unit.empty()) {
val_str += " " + unit;
}
return val_str;

View File

@@ -1,29 +1,35 @@
#pragma once
#include <cstdio>
#include <cstdlib>
#include <functional>
#include <limits>
#include <string>
#include <utility>
#include <vector>
#include <QColor>
#include <QMetaType>
#include <QString>
const QString UNTITLED = "untitled";
const QString DEFAULT_NODE_NAME = "XXX";
const std::string UNTITLED = "untitled";
const std::string DEFAULT_NODE_NAME = "XXX";
constexpr int CAN_MAX_DATA_BYTES = 64;
struct MessageId {
uint8_t source = 0;
uint32_t address = 0;
QString toString() const {
return QString("%1:%2").arg(source).arg(QString::number(address, 16).toUpper());
std::string toString() const {
char buf[64];
snprintf(buf, sizeof(buf), "%u:%X", source, address);
return buf;
}
inline static MessageId fromString(const QString &str) {
auto parts = str.split(':');
if (parts.size() != 2) return {};
return MessageId{.source = uint8_t(parts[0].toUInt()), .address = parts[1].toUInt(nullptr, 16)};
inline static MessageId fromString(const std::string &str) {
auto pos = str.find(':');
if (pos == std::string::npos) return {};
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 {
@@ -43,15 +49,17 @@ struct MessageId {
}
};
uint qHash(const MessageId &item);
Q_DECLARE_METATYPE(MessageId);
template <>
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 {
@@ -61,7 +69,7 @@ public:
Signal(const Signal &other) = default;
void update();
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;
inline bool operator!=(const cabana::Signal &other) const { return !(*this == other); }
@@ -72,16 +80,16 @@ public:
};
Type type = Type::Normal;
QString name;
std::string name;
int start_bit, msb, lsb, size;
double factor = 1.0;
double offset = 0;
bool is_signed;
bool is_little_endian;
double min, max;
QString unit;
QString comment;
QString receiver_name;
std::string unit;
std::string comment;
std::string receiver_name;
ValueDescription val_desc;
int precision = 0;
QColor color;
@@ -97,20 +105,20 @@ public:
Msg(const Msg &other) { *this = other; }
~Msg();
cabana::Signal *addSignal(const cabana::Signal &sig);
cabana::Signal *updateSignal(const QString &sig_name, const cabana::Signal &sig);
void removeSignal(const QString &sig_name);
cabana::Signal *updateSignal(const std::string &sig_name, const cabana::Signal &sig);
void removeSignal(const std::string &sig_name);
Msg &operator=(const Msg &other);
int indexOf(const cabana::Signal *sig) const;
cabana::Signal *sig(const QString &sig_name) const;
QString newSignalName();
cabana::Signal *sig(const std::string &sig_name) const;
std::string newSignalName();
void update();
inline const std::vector<cabana::Signal *> &getSignals() const { return sigs; }
uint32_t address;
QString name;
std::string name;
uint32_t size;
QString comment;
QString transmitter;
std::string comment;
std::string transmitter;
std::vector<cabana::Signal *> sigs;
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);
void updateMsbLsb(cabana::Signal &s);
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;
}

View File

@@ -3,11 +3,12 @@
#include <QFile>
#include <QFileInfo>
#include <QRegularExpression>
#include <QString>
DBCFile::DBCFile(const QString &dbc_file_name) {
QFile file(dbc_file_name);
DBCFile::DBCFile(const std::string &dbc_file_name) {
QFile file(QString::fromStdString(dbc_file_name));
if (file.open(QIODevice::ReadOnly)) {
name_ = QFileInfo(dbc_file_name).baseName();
name_ = QFileInfo(QString::fromStdString(dbc_file_name)).baseName().toStdString();
filename = dbc_file_name;
parse(file.readAll());
} else {
@@ -15,34 +16,35 @@ DBCFile::DBCFile(const QString &dbc_file_name) {
}
}
DBCFile::DBCFile(const QString &name, const QString &content) : name_(name), filename("") {
parse(content);
DBCFile::DBCFile(const std::string &name, const std::string &content) : name_(name), filename("") {
parse(QString::fromStdString(content));
}
bool DBCFile::save() {
assert(!filename.isEmpty());
assert(!filename.empty());
return writeContents(filename);
}
bool DBCFile::saveAs(const QString &new_filename) {
bool DBCFile::saveAs(const std::string &new_filename) {
filename = new_filename;
return save();
}
bool DBCFile::writeContents(const QString &fn) {
QFile file(fn);
bool DBCFile::writeContents(const std::string &fn) {
QFile file(QString::fromStdString(fn));
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;
}
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];
m.address = id.address;
m.name = name;
m.size = size;
m.transmitter = node.isEmpty() ? DEFAULT_NODE_NAME : node;
m.transmitter = node.empty() ? DEFAULT_NODE_NAME : node;
m.comment = comment;
}
@@ -51,12 +53,12 @@ cabana::Msg *DBCFile::msg(uint32_t address) {
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; });
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);
return m ? (cabana::Signal *)m->sig(name) : nullptr;
}
@@ -93,13 +95,13 @@ void DBCFile::parse(const QString &content) {
seen = false;
}
} 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) {
seen_first = true;
} 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
cabana::Msg *msg = &msgs[address];
msg->address = address;
msg->name = match.captured("name");
msg->name = match.captured("name").toStdString();
msg->size = match.captured("size").toULong();
msg->transmitter = match.captured("transmitter").trimmed();
msg->transmitter = match.captured("transmitter").trimmed().toStdString();
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");
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) {
@@ -160,7 +162,7 @@ void DBCFile::parseSG(const QString &line, cabana::Msg *current_msg, int &multip
if (!match.hasMatch())
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)
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.min = match.captured(8 + offset).toDouble();
s.max = match.captured(9 + offset).toDouble();
s.unit = match.captured(10 + offset);
s.receiver_name = match.captured(11 + offset).trimmed();
s.unit = match.captured(10 + offset).toStdString();
s.receiver_name = match.captured(11 + offset).trimmed().toStdString();
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())
throw std::runtime_error("Invalid CM_ SG_ line format");
if (auto s = signal(match.captured(1).toUInt(), match.captured(2))) {
s->comment = match.captured(3).trimmed().replace("\\\"", "\"");
if (auto s = signal(match.captured(1).toUInt(), match.captured(2).toStdString())) {
s->comment = match.captured(3).trimmed().replace("\\\"", "\"").toStdString();
}
}
@@ -217,55 +219,60 @@ void DBCFile::parseVAL(const QString &line) {
if (!match.hasMatch())
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('"');
for (int i = 0; i < desc_list.size(); i += 2) {
auto val = desc_list[i].trimmed();
if (!val.isEmpty() && (i + 1) < desc_list.size()) {
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() {
QString dbc_string, comment, val_desc;
std::string DBCFile::generateDBC() {
std::string dbc_string, comment, val_desc;
for (const auto &[address, m] : msgs) {
const QString transmitter = m.transmitter.isEmpty() ? DEFAULT_NODE_NAME : m.transmitter;
dbc_string += QString("BO_ %1 %2: %3 %4\n").arg(address).arg(m.name).arg(m.size).arg(transmitter);
if (!m.comment.isEmpty()) {
comment += QString("CM_ BO_ %1 \"%2\";\n").arg(address).arg(QString(m.comment).replace("\"", "\\\""));
const std::string &transmitter = m.transmitter.empty() ? DEFAULT_NODE_NAME : m.transmitter;
dbc_string += "BO_ " + std::to_string(address) + " " + m.name + ": " + std::to_string(m.size) + " " + transmitter + "\n";
if (!m.comment.empty()) {
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()) {
QString multiplexer_indicator;
std::string multiplexer_indicator;
if (sig->type == cabana::Signal::Type::Multiplexor) {
multiplexer_indicator = "M ";
} 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")
.arg(sig->name)
.arg(multiplexer_indicator)
.arg(sig->start_bit)
.arg(sig->size)
.arg(sig->is_little_endian ? '1' : '0')
.arg(sig->is_signed ? '-' : '+')
.arg(doubleToString(sig->factor))
.arg(doubleToString(sig->offset))
.arg(doubleToString(sig->min))
.arg(doubleToString(sig->max))
.arg(sig->unit)
.arg(sig->receiver_name.isEmpty() ? DEFAULT_NODE_NAME : sig->receiver_name);
if (!sig->comment.isEmpty()) {
comment += QString("CM_ SG_ %1 %2 \"%3\";\n").arg(address).arg(sig->name).arg(QString(sig->comment).replace("\"", "\\\""));
const std::string &recv = sig->receiver_name.empty() ? DEFAULT_NODE_NAME : sig->receiver_name;
dbc_string += " SG_ " + sig->name + " " + multiplexer_indicator + ": " +
std::to_string(sig->start_bit) + "|" + std::to_string(sig->size) + "@" +
std::string(1, sig->is_little_endian ? '1' : '0') +
std::string(1, sig->is_signed ? '-' : '+') +
" (" + doubleToString(sig->factor) + "," + doubleToString(sig->offset) + ")" +
" [" + doubleToString(sig->min) + "|" + doubleToString(sig->max) + "]" +
" \"" + sig->unit + "\" " + recv + "\n";
if (!sig->comment.empty()) {
std::string escaped_comment = sig->comment;
for (size_t pos = 0; (pos = escaped_comment.find('"', pos)) != std::string::npos; pos += 2)
escaped_comment.replace(pos, 1, "\\\"");
comment += "CM_ SG_ " + std::to_string(address) + " " + sig->name + " \"" + escaped_comment + "\";\n";
}
if (!sig->val_desc.empty()) {
QStringList text;
std::string text;
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";

View File

@@ -1,34 +1,35 @@
#pragma once
#include <map>
#include <string>
#include <QTextStream>
#include "tools/cabana/dbc/dbc.h"
class DBCFile {
public:
DBCFile(const QString &dbc_file_name);
DBCFile(const QString &name, const QString &content);
DBCFile(const std::string &dbc_file_name);
DBCFile(const std::string &name, const std::string &content);
~DBCFile() {}
bool save();
bool saveAs(const QString &new_filename);
bool writeContents(const QString &fn);
QString generateDBC();
bool saveAs(const std::string &new_filename);
bool writeContents(const std::string &fn);
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 const std::map<uint32_t, cabana::Msg> &getMessages() const { return msgs; }
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); }
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 bool isEmpty() const { return msgs.empty() && name_.isEmpty(); }
inline std::string name() const { return name_.empty() ? "untitled" : name_; }
inline bool isEmpty() const { return msgs.empty() && name_.empty(); }
QString filename;
std::string filename;
private:
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 parseVAL(const QString &line);
QString header;
std::string header;
std::map<uint32_t, cabana::Msg> msgs;
QString name_;
std::string name_;
};

View File

@@ -1,10 +1,9 @@
#include "tools/cabana/dbc/dbcmanager.h"
#include <QSet>
#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 {
auto it = std::find_if(dbc_files.begin(), dbc_files.end(),
[&](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;
}
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 {
auto file = std::make_shared<DBCFile>(name, content);
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 s = m->updateSignal(sig_name, sig)) {
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 s = m->sig(sig_name)) {
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);
assert(dbc_file); // This should be impossible
dbc_file->updateMsg(id, name, size, node, comment);
@@ -98,11 +97,13 @@ void DBCManager::removeMsg(const MessageId &id) {
emit maskUpdated();
}
QString DBCManager::newMsgName(const MessageId &id) {
return QString("NEW_MSG_") + QString::number(id.address, 16).toUpper();
std::string DBCManager::newMsgName(const MessageId &id) {
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);
return m ? m->newSignalName() : "";
}
@@ -118,14 +119,14 @@ cabana::Msg *DBCManager::msg(const MessageId &id) {
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);
return dbc_file ? dbc_file->msg(name) : nullptr;
}
QStringList DBCManager::signalNames() {
std::vector<std::string> DBCManager::signalNames() {
// Used for autocompletion
QSet<QString> names;
std::set<std::string> names;
for (auto &f : allDBCFiles()) {
for (auto &[_, m] : f->getMessages()) {
for (auto sig : m.getSignals()) {
@@ -133,8 +134,8 @@ QStringList DBCManager::signalNames() {
}
}
}
QStringList ret = names.values();
ret.sort();
std::vector<std::string> ret(names.begin(), names.end());
std::sort(ret.begin(), ret.end());
return ret;
}
@@ -165,11 +166,13 @@ const SourceSet DBCManager::sources(const DBCFile *dbc_file) const {
return sources;
}
QString toString(const SourceSet &ss) {
return std::accumulate(ss.cbegin(), ss.cend(), QString(), [](QString str, int source) {
if (!str.isEmpty()) str += ", ";
return str + (source == -1 ? QStringLiteral("all") : QString::number(source));
});
std::string toString(const SourceSet &ss) {
std::string result;
for (int source : ss) {
if (!result.empty()) result += ", ";
result += (source == -1) ? "all" : std::to_string(source);
}
return result;
}
DBCManager *dbc() {

View File

@@ -4,6 +4,8 @@
#include <memory>
#include <map>
#include <set>
#include <string>
#include <vector>
#include "tools/cabana/dbc/dbcfile.h"
@@ -18,27 +20,27 @@ class DBCManager : public QObject {
public:
DBCManager(QObject *parent) : QObject(parent) {}
~DBCManager() {}
bool open(const SourceSet &sources, const QString &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 &dbc_file_name, 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(DBCFile *dbc_file);
void closeAll();
void addSignal(const MessageId &id, const cabana::Signal &sig);
void updateSignal(const MessageId &id, const QString &sig_name, const cabana::Signal &sig);
void removeSignal(const MessageId &id, const QString &sig_name);
void updateSignal(const MessageId &id, const std::string &sig_name, const cabana::Signal &sig);
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);
QString newMsgName(const MessageId &id);
QString newSignalName(const MessageId &id);
std::string newMsgName(const MessageId &id);
std::string newSignalName(const MessageId &id);
const std::map<uint32_t, cabana::Msg> &getMessages(uint8_t source);
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(); }
int nonEmptyDBCCount();
@@ -62,8 +64,8 @@ private:
DBCManager *dbc();
QString toString(const SourceSet &ss);
inline QString msgName(const MessageId &id) {
std::string toString(const SourceSet &ss);
inline std::string msgName(const MessageId &id) {
auto msg = dbc()->msg(id);
return msg ? msg->name : UNTITLED;
}

View File

@@ -124,9 +124,9 @@ int DetailWidget::findOrAddTab(const MessageId& message_id) {
if (tabbar->tabData(index).value<MessageId>() == message_id) break;
}
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->setTabToolTip(index, msgName(message_id));
tabbar->setTabToolTip(index, QString::fromStdString(msgName(message_id)));
}
return index;
}
@@ -151,21 +151,21 @@ std::pair<QString, QStringList> DetailWidget::serializeMessageIds() const {
QStringList msgs;
for (int i = 0; i < tabbar->count(); ++i) {
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) {
tabbar->blockSignals(true);
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)
findOrAddTab(id);
}
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)
setMessage(active_id);
}
@@ -180,10 +180,10 @@ void DetailWidget::refresh() {
warnings.push_back(tr("Message size (%1) is incorrect.").arg(msg->size));
}
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->setToolTip(msg_name);
action_remove_msg->setEnabled(msg != nullptr);
@@ -208,10 +208,10 @@ void DetailWidget::updateState(const std::set<MessageId> *msgs) {
void DetailWidget::editMsg() {
auto msg = dbc()->msg(msg_id);
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()) {
UndoStack::push(new EditMsgCommand(msg_id, dlg.name_edit->text().trimmed(), dlg.size_spin->value(),
dlg.node->text().trimmed(), dlg.comment_edit->toPlainText().trimmed()));
UndoStack::push(new EditMsgCommand(msg_id, dlg.name_edit->text().trimmed().toStdString(), dlg.size_spin->value(),
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)
: 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);
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));
if (auto msg = dbc()->msg(msg_id)) {
node->setText(msg->transmitter);
comment_edit->setText(msg->comment);
node->setText(QString::fromStdString(msg->transmitter));
comment_edit->setText(QString::fromStdString(msg->comment));
}
validateName(name_edit->text());
setFixedWidth(parent->width() * 0.9);
@@ -252,10 +252,10 @@ EditMessageDialog::EditMessageDialog(const MessageId &msg_id, const QString &tit
}
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);
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) {
error_label->setText(tr("Name already exists"));
error_label->setVisible(true);

View File

@@ -14,7 +14,7 @@ QVariant HistoryLogModel::data(const QModelIndex &index, int role) const {
const int col = index.column();
if (role == Qt::DisplayRole) {
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) {
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 (isHexMode()) return "Data";
QString name = sigs[section - 1]->name;
QString unit = sigs[section - 1]->unit;
QString name = QString::fromStdString(sigs[section - 1]->name);
QString unit = QString::fromStdString(sigs[section - 1]->unit);
return unit.isEmpty() ? name : QString("%1 (%2)").arg(name, unit);
} else if (role == Qt::BackgroundRole && section > 0 && !isHexMode()) {
// Alpha-blend the signal color with the background to ensure contrast
@@ -216,7 +216,7 @@ LogsWidget::LogsWidget(QWidget *parent) : QFrame(parent) {
void LogsWidget::modelReset() {
signals_cb->clear();
for (auto s : model->sigs) {
signals_cb->addItem(s->name);
signals_cb->addItem(QString::fromStdString(s->name));
}
export_btn->setEnabled(false);
value_edit->clear();
@@ -238,8 +238,8 @@ void LogsWidget::filterChanged() {
}
void LogsWidget::exportToCSV() {
QString dir = QString("%1/%2_%3.csv").arg(settings.last_dir).arg(can->routeName()).arg(msgName(model->msg_id));
QString fn = QFileDialog::getSaveFileName(this, QString("Export %1 to CSV file").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(QString::fromStdString(msgName(model->msg_id))),
dir, tr("csv (*.csv)"));
if (!fn.isEmpty()) {
model->isHexMode() ? utils::exportToCSV(fn, model->msg_id)

View File

@@ -234,7 +234,7 @@ void MainWindow::DBCFileChanged() {
QStringList title;
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(" | "));
@@ -259,7 +259,7 @@ void MainWindow::closeStream() {
}
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)"));
if (!fn.isEmpty()) {
utils::exportToCSV(fn);
@@ -268,7 +268,7 @@ void MainWindow::exportToCSV() {
void MainWindow::newFile(SourceSet s) {
closeFile(s);
dbc()->open(s, "", "");
dbc()->open(s, std::string(""), std::string(""));
}
void MainWindow::openFile(SourceSet s) {
@@ -284,7 +284,7 @@ void MainWindow::loadFile(const QString &fn, SourceSet s) {
closeFile(s);
QString error;
if (dbc()->open(s, fn, &error)) {
if (dbc()->open(s, fn.toStdString(), &error)) {
updateRecentFiles(fn);
statusBar()->showMessage(tr("DBC File %1 loaded").arg(fn), 2000);
} else {
@@ -304,7 +304,7 @@ void MainWindow::loadFromClipboard(SourceSet s, bool close_all) {
QString dbc_str = QGuiApplication::clipboard()->text();
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) {
QMessageBox::information(this, tr("Load From Clipboard"), tr("DBC Successfully Loaded!"));
} else {
@@ -333,7 +333,7 @@ void MainWindow::startStream(AbstractStream *stream, QString dbc_file) {
can->start();
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;
close_stream_act->setEnabled(has_stream);
@@ -341,7 +341,7 @@ void MainWindow::startStream(AbstractStream *stream, QString dbc_file) {
tools_menu->setEnabled(has_stream);
createDockWidgets();
video_dock->setWindowTitle(can->routeName());
video_dock->setWindowTitle(QString::fromStdString(can->routeName()));
if (can->liveStreaming() || video_splitter->sizes()[0] == 0) {
// display video at minimum size.
video_splitter->setSizes({1, 1});
@@ -368,9 +368,9 @@ void MainWindow::startStream(AbstractStream *stream, QString dbc_file) {
}
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")
.arg(can->routeName())
.arg(QString::fromStdString(can->routeName()))
.arg(car_fingerprint.isEmpty() ? tr("Unknown Car") : car_fingerprint));
// Don't overwrite already loaded DBC
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) {
assert(dbc_file != nullptr);
if (!dbc_file->filename.isEmpty()) {
if (!dbc_file->filename.empty()) {
dbc_file->save();
UndoStack::instance()->setClean();
statusBar()->showMessage(tr("File saved"), 2000);
@@ -426,10 +426,10 @@ void MainWindow::saveFile(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)"));
if (!fn.isEmpty()) {
dbc_file->saveAs(fn);
dbc_file->saveAs(fn.toStdString());
UndoStack::instance()->setClean();
statusBar()->showMessage(tr("File saved as %1").arg(fn), 2000);
updateRecentFiles(fn);
@@ -446,7 +446,7 @@ void MainWindow::saveToClipboard() {
void MainWindow::saveFileToClipboard(DBCFile *dbc_file) {
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!"));
}
@@ -467,14 +467,14 @@ void MainWindow::updateLoadSaveMenus() {
auto dbc_file = dbc()->findDBCFile(source);
if (dbc_file) {
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 As..."), [=]() { saveFileAs(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 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);
}
@@ -627,7 +627,7 @@ void MainWindow::saveSessionState() {
settings.active_charts.clear();
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()) {
auto [active_id, ids] = detail->serializeMessageIds();
@@ -643,7 +643,7 @@ void MainWindow::restoreSessionState() {
QString dbc_file;
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 (!settings.selected_msg_ids.isEmpty())

View File

@@ -205,7 +205,7 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const {
} else if (role == Qt::ToolTipRole && index.column() == Column::NAME) {
auto msg = dbc()->msg(item.id);
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 {};
@@ -277,7 +277,7 @@ bool MessageListModel::match(const MessageListModel::Item &item) {
if (!match) {
const auto m = dbc()->msg(item.id);
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;
}
@@ -323,8 +323,8 @@ bool MessageListModel::filterAndSort() {
if (show_inactive_messages || can->isMessageActive(id)) {
auto msg = dbc()->msg(id);
Item item = {.id = id,
.name = msg ? msg->name : UNTITLED,
.node = msg ? msg->transmitter : QString()};
.name = msg ? QString::fromStdString(msg->name) : QString::fromStdString(UNTITLED),
.node = msg ? QString::fromStdString(msg->transmitter) : QString()};
if (match(item))
items.emplace_back(item);
}

View File

@@ -1,6 +1,7 @@
#include "tools/cabana/signalview.h"
#include <algorithm>
#include <future>
#include <QCompleter>
#include <QDialogButtonBox>
@@ -11,7 +12,6 @@
#include <QPainterPath>
#include <QPushButton>
#include <QScrollBar>
#include <QtConcurrent>
#include <QVBoxLayout>
#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) {
Item *parent_item = new Item{.type = Item::Sig, .parent = root_item, .sig = sig, .title = sig->name};
root_item->children.insert(pos, parent_item);
Item *parent_item = new Item{.type = Item::Sig, .parent = root_item, .sig = sig, .title = QString::fromStdString(sig->name)};
root_item->children.insert(root_item->children.begin() + pos, parent_item);
QString titles[]{"Name", "Size", "Receiver Nodes", "Little Endian", "Signed", "Offset", "Factor", "Type",
"Multiplex Value", "Extra Info", "Unit", "Comment", "Minimum Value", "Maximum Value", "Value Table"};
for (int i = 0; i < std::size(titles); ++i) {
@@ -63,7 +63,7 @@ void SignalModel::refresh() {
root.reset(new SignalModel::Item);
if (auto msg = dbc()->msg(msg_id)) {
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);
}
}
@@ -124,25 +124,25 @@ QVariant SignalModel::data(const QModelIndex &index, int role) const {
const Item *item = getItem(index);
if (role == Qt::DisplayRole || role == Qt::EditRole) {
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 {
switch (item->type) {
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::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::MultiplexValue: return item->sig->multiplex_value;
case Item::Offset: return doubleToString(item->sig->offset);
case Item::Factor: return doubleToString(item->sig->factor);
case Item::Unit: return item->sig->unit;
case Item::Comment: return item->sig->comment;
case Item::Min: return doubleToString(item->sig->min);
case Item::Max: return doubleToString(item->sig->max);
case Item::Offset: return QString::fromStdString(doubleToString(item->sig->offset));
case Item::Factor: return QString::fromStdString(doubleToString(item->sig->factor));
case Item::Unit: return QString::fromStdString(item->sig->unit);
case Item::Comment: return QString::fromStdString(item->sig->comment);
case Item::Min: return QString::fromStdString(doubleToString(item->sig->min));
case Item::Max: return QString::fromStdString(doubleToString(item->sig->max));
case Item::Desc: {
QStringList 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(" ");
}
@@ -165,17 +165,17 @@ bool SignalModel::setData(const QModelIndex &index, const QVariant &value, int r
Item *item = getItem(index);
cabana::Signal s = *item->sig;
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::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::MultiplexValue: s.multiplex_value = value.toInt(); break;
case Item::Endian: s.is_little_endian = value.toBool(); break;
case Item::Signed: s.is_signed = value.toBool(); break;
case Item::Offset: s.offset = value.toDouble(); break;
case Item::Factor: s.factor = value.toDouble(); break;
case Item::Unit: s.unit = value.toString(); break;
case Item::Comment: s.comment = value.toString(); break;
case Item::Unit: s.unit = value.toString().toStdString(); break;
case Item::Comment: s.comment = value.toString().toStdString(); break;
case Item::Min: s.min = value.toDouble(); break;
case Item::Max: s.max = value.toDouble(); 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) {
auto msg = dbc()->msg(msg_id);
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);
return false;
}
@@ -214,7 +214,7 @@ void SignalModel::handleSignalAdded(MessageId id, const cabana::Signal *sig) {
beginInsertRows({}, i, i);
insertItem(root.get(), i, sig);
endInsertRows();
} else if (sig->name.contains(filter_str, Qt::CaseInsensitive)) {
} else if (QString::fromStdString(sig->name).contains(filter_str, Qt::CaseInsensitive)) {
refresh();
}
}
@@ -229,7 +229,9 @@ void SignalModel::handleSignalUpdated(const cabana::Signal *sig) {
int to = dbc()->msg(msg_id)->indexOf(sig);
if (to != row) {
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();
}
}
@@ -239,7 +241,8 @@ void SignalModel::handleSignalUpdated(const cabana::Signal *sig) {
void SignalModel::handleSignalRemoved(const cabana::Signal *sig) {
if (int row = signalRow(sig); row != -1) {
beginRemoveRows({}, row, row);
delete root->children.takeAt(row);
delete root->children[row];
root->children.erase(root->children.begin() + row);
endRemoveRows();
}
}
@@ -373,7 +376,10 @@ QWidget *SignalItemDelegate::createEditor(QWidget *parent, const QStyleOptionVie
else e->setValidator(double_validator);
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->setFilterMode(Qt::MatchContains);
e->setCompleter(completer);
@@ -395,7 +401,7 @@ QWidget *SignalItemDelegate::createEditor(QWidget *parent, const QStyleOptionVie
return c;
} else if (item->type == SignalModel::Item::Desc) {
ValueDescriptionDlg dlg(item->sig->val_desc, parent);
dlg.setWindowTitle(item->sig->name);
dlg.setWindowTitle(QString::fromStdString(item->sig->name));
if (dlg.exec()) {
((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) {
double value = 0;
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));
}
}
@@ -635,13 +641,13 @@ void SignalView::updateState(const std::set<MessageId> *msgs) {
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));
QFutureSynchronizer<void> synchronizer;
std::vector<std::future<void>> futures;
for (int i = first_visible.row(); i <= last_visible.row(); ++i) {
auto item = model->getItem(model->index(i, 1));
synchronizer.addFuture(QtConcurrent::run(
&item->sparkline, &Sparkline::update, item->sig, first, last, settings.sparkline_range, size));
futures.push_back(std::async(std::launch::async,
&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) {
@@ -677,7 +683,7 @@ ValueDescriptionDlg::ValueDescriptionDlg(const ValueDescription &descriptions, Q
int row = 0;
for (auto &[val, desc] : descriptions) {
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;
}
@@ -706,7 +712,7 @@ void ValueDescriptionDlg::save() {
QString val = table->item(i, 0)->text().trimmed();
QString desc = table->item(i, 1)->text().trimmed();
if (!val.isEmpty() && !desc.isEmpty()) {
val_desc.push_back({val.toDouble(), desc});
val_desc.push_back({val.toDouble(), desc.toStdString()});
}
}
QDialog::accept();

View File

@@ -20,12 +20,15 @@ class SignalModel : public QAbstractItemModel {
public:
struct Item {
enum Type {Root, Sig, Name, Size, Node, Endian, Signed, Offset, Factor, SignalType, MultiplexValue, ExtraInfo, Unit, Comment, Min, Max, Desc };
~Item() { qDeleteAll(children); }
inline int row() { return parent->children.indexOf(this); }
~Item() { for (auto c : children) delete c; }
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;
Item *parent = nullptr;
QList<Item *> children;
std::vector<Item *> children;
const cabana::Signal *sig = nullptr;
QString title;

View File

@@ -65,8 +65,8 @@ public:
virtual void start() = 0;
virtual bool liveStreaming() const { return true; }
virtual void seekTo(double ts) {}
virtual QString routeName() const = 0;
virtual QString carFingerprint() const { return ""; }
virtual std::string routeName() const = 0;
virtual std::string carFingerprint() const { return ""; }
virtual QDateTime beginDateTime() const { return {}; }
virtual uint64_t beginMonoTime() const { return 0; }
virtual double minSeconds() const { return 0; }
@@ -149,7 +149,7 @@ class DummyStream : public AbstractStream {
Q_OBJECT
public:
DummyStream(QObject *parent) : AbstractStream(parent) {}
QString routeName() const override { return tr("No Stream"); }
std::string routeName() const override { return "No Stream"; }
void start() override {}
};

View File

@@ -9,8 +9,8 @@ class DeviceStream : public LiveStream {
public:
DeviceStream(QObject *parent, QString address = {});
~DeviceStream();
inline QString routeName() const override {
return QString("Live Streaming From %1").arg(zmq_address.isEmpty() ? "127.0.0.1" : zmq_address);
inline std::string routeName() const override {
return "Live Streaming From " + (zmq_address.isEmpty() ? std::string("127.0.0.1") : zmq_address.toStdString());
}
protected:

View File

@@ -16,8 +16,8 @@ PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(co
bool PandaStream::connect() {
try {
qDebug() << "Connecting to panda " << config.serial;
panda.reset(new Panda(config.serial.toStdString()));
qDebug() << "Connecting to panda " << config.serial.c_str();
panda.reset(new Panda(config.serial));
config.bus_config.resize(3);
qDebug() << "Connected";
} catch (const std::exception& e) {
@@ -81,7 +81,7 @@ void PandaStream::streamThread() {
OpenPandaWidget::OpenPandaWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) {
form_layout = new QFormLayout(this);
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."));
QTimer::singleShot(0, [this]() { emit enableOpenButton(false); });
return;
@@ -129,7 +129,7 @@ void OpenPandaWidget::buildConfigForm() {
}
if (has_panda) {
config.serial = serial;
config.serial = serial.toStdString();
config.bus_config.resize(3);
for (int i = 0; i < config.bus_config.size(); i++) {
QHBoxLayout *bus_layout = new QHBoxLayout;

View File

@@ -19,7 +19,7 @@ struct BusConfig {
};
struct PandaStreamConfig {
QString serial = "";
std::string serial = "";
std::vector<BusConfig> bus_config;
};
@@ -28,8 +28,8 @@ class PandaStream : public LiveStream {
public:
PandaStream(QObject *parent, PandaStreamConfig config_ = {});
~PandaStream() { stop(); }
inline QString routeName() const override {
return QString("Panda: %1").arg(config.serial);
inline std::string routeName() const override {
return "Panda: " + config.serial;
}
protected:

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) {
replay.reset(new Replay(route.toStdString(), {"can", "roadEncodeIdx", "driverEncodeIdx", "wideRoadEncodeIdx", "carParams"},
{}, nullptr, replay_flags, data_dir.toStdString(), 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, {"can", "roadEncodeIdx", "driverEncodeIdx", "wideRoadEncodeIdx", "carParams"},
{}, nullptr, replay_flags, data_dir, auto_source));
replay->setSegmentCacheLimit(settings.max_cached_minutes);
replay->installEventFilter([this](const Event *event) { return eventFilter(event); });
@@ -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.";
} else {
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);
} else if (replay->lastRouteError() == RouteLoadError::NetworkError) {
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) {
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 {
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;
@@ -168,7 +168,7 @@ AbstractStream *OpenReplayWidget::open() {
if (cameras[2]->isChecked()) flags |= REPLAY_FLAG_ECAM;
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();
}
}

View File

@@ -18,12 +18,12 @@ class ReplayStream : public AbstractStream {
public:
ReplayStream(QObject *parent);
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);
void seekTo(double ts) override { replay->seekTo(std::max(double(0), ts), false); }
bool liveStreaming() const override { return false; }
inline QString routeName() const override { return QString::fromStdString(replay->route().name()); }
inline QString carFingerprint() const override { return replay->carFingerprint().c_str(); }
inline std::string routeName() const override { return replay->route().name(); }
inline std::string carFingerprint() const override { return replay->carFingerprint(); }
double minSeconds() const override { return replay->minSeconds(); }
double maxSeconds() const { return replay->maxSeconds(); }
inline QDateTime beginDateTime() const { return QDateTime::fromSecsSinceEpoch(replay->routeDateTime()); }

View File

@@ -11,7 +11,7 @@
#include <QMessageBox>
#include <QPainter>
#include <QPointer>
#include <QtConcurrent>
#include <thread>
#include "tools/replay/py_downloader.h"
@@ -72,13 +72,12 @@ RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) {
// Fetch devices
QPointer<RoutesDialog> self = this;
QtConcurrent::run([self]() {
std::thread([self]() {
std::string result = PyDownloader::getDevices();
auto [success, error_code] = checkApiResponse(result);
QMetaObject::invokeMethod(qApp, [self, r = QString::fromStdString(result), success, error_code]() {
if (self) self->parseDeviceList(r, success, error_code);
QMetaObject::invokeMethod(qApp, [self, r = QString::fromStdString(result), response = checkApiResponse(result)]() {
if (self) self->parseDeviceList(r, response.first, response.second);
}, Qt::QueuedConnection);
});
}).detach();
}
void RoutesDialog::parseDeviceList(const QString &json, bool success, int error_code) {
@@ -114,14 +113,13 @@ void RoutesDialog::fetchRoutes() {
int request_id = ++fetch_id_;
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);
if (!self || self->fetch_id_ != request_id) return;
auto [success, error_code] = checkApiResponse(result);
QMetaObject::invokeMethod(qApp, [self, r = QString::fromStdString(result), success, error_code, request_id]() {
if (self && self->fetch_id_ == request_id) self->parseRouteList(r, success, error_code);
QMetaObject::invokeMethod(qApp, [self, r = QString::fromStdString(result), response = checkApiResponse(result), request_id]() {
if (self && self->fetch_id_ == request_id) self->parseRouteList(r, response.first, response.second);
}, Qt::QueuedConnection);
});
}).detach();
}
void RoutesDialog::parseRouteList(const QString &json, bool success, int error_code) {

View File

@@ -1,6 +1,14 @@
#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 <QDir>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QMessageBox>
@@ -9,59 +17,82 @@
SocketCanStream::SocketCanStream(QObject *parent, SocketCanStreamConfig config_) : config(config_), LiveStream(parent) {
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()) {
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() {
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() {
// Connecting might generate some warnings about missing socketcan/libsocketcan libraries
// These are expected and can be ignored, we don't need the advanced features of libsocketcan
QString errorString;
device.reset(QCanBus::instance()->createDevice("socketcan", config.device, &errorString));
device->setConfigurationParameter(QCanBusDevice::CanFdKey, true);
if (!device) {
qDebug() << "Failed to create SocketCAN device" << errorString;
sock_fd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if (sock_fd < 0) {
qDebug() << "Failed to create CAN socket";
return false;
}
if (!device->connectDevice()) {
qDebug() << "Failed to connect to device";
// Enable CAN-FD
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;
}
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;
}
void SocketCanStream::streamThread() {
while (!QThread::currentThread()->isInterruptionRequested()) {
QThread::msleep(1);
struct canfd_frame frame;
auto frames = device->readAllFrames();
if (frames.size() == 0) continue;
while (!QThread::currentThread()->isInterruptionRequested()) {
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;
auto evt = msg.initEvent();
auto canData = evt.initCan(frames.size());
for (uint i = 0; i < frames.size(); i++) {
if (!frames[i].isValid()) continue;
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()));
}
auto canData = evt.initCan(1);
canData[0].setAddress(frame.can_id & CAN_EFF_MASK);
canData[0].setSrc(0);
canData[0].setDat(kj::arrayPtr(frame.data, len));
handleEvent(capnp::messageToFlatArray(msg));
}
@@ -87,7 +118,7 @@ OpenSocketCanWidget::OpenSocketCanWidget(QWidget *parent) : AbstractOpenStreamWi
main_layout->addStretch(1);
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
refreshDevices();
@@ -95,12 +126,19 @@ OpenSocketCanWidget::OpenSocketCanWidget(QWidget *parent) : AbstractOpenStreamWi
void OpenSocketCanWidget::refreshDevices() {
device_edit->clear();
for (auto device : QCanBus::instance()->availableDevices(QStringLiteral("socketcan"))) {
device_edit->addItem(device.name());
// Scan /sys/class/net/ for CAN interfaces (type 280 = ARPHRD_CAN)
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() {
try {
return new SocketCanStream(qApp, config);

View File

@@ -1,27 +1,22 @@
#pragma once
#include <memory>
#include <QtSerialBus/QCanBus>
#include <QtSerialBus/QCanBusDevice>
#include <QtSerialBus/QCanBusDeviceInfo>
#include <QComboBox>
#include "tools/cabana/streams/livestream.h"
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 {
Q_OBJECT
public:
SocketCanStream(QObject *parent, SocketCanStreamConfig config_ = {});
~SocketCanStream() { stop(); }
~SocketCanStream();
static bool available();
inline QString routeName() const override {
return QString("Live Streaming From Socket CAN %1").arg(config.device);
inline std::string routeName() const override {
return "Live Streaming From Socket CAN " + config.device;
}
protected:
@@ -29,7 +24,7 @@ protected:
bool connect();
SocketCanStreamConfig config = {};
std::unique_ptr<QCanBusDevice> device;
int sock_fd = -1;
};
class OpenSocketCanWidget : public AbstractOpenStreamWidget {

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";
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_from_generated("", dbc_origin.generateDBC());
@@ -30,7 +30,7 @@ TEST_CASE("DBCFile::generateDBC") {
TEST_CASE("DBCFile::generateDBC - comment 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
BO_ 162 message_2: 8 EON
@@ -46,7 +46,7 @@ CM_ SG_ 162 signal_2 "signal comment";
}
TEST_CASE("DBCFile::generateDBC -- preserve original header") {
QString content = R"(VERSION "1.0"
std::string content = R"(VERSION "1.0"
NS_ :
CM_
@@ -66,7 +66,7 @@ CM_ SG_ 160 signal_1 "signal comment";
}
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
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") {
QString content = R"(
std::string content = R"(
BO_ 160 message_1: 8 EON
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
@@ -119,9 +119,9 @@ CM_ SG_ 162 signal_1 "signal comment with \"escaped quotes\"";
REQUIRE(sig_1->comment == "signal comment");
REQUIRE(sig_1->receiver_name == "XXX");
REQUIRE(sig_1->val_desc.size() == 3);
REQUIRE(sig_1->val_desc[0] == std::pair<double, QString>{0, "disabled"});
REQUIRE(sig_1->val_desc[1] == std::pair<double, QString>{1.2, "initializing"});
REQUIRE(sig_1->val_desc[2] == std::pair<double, QString>{2, "fault"});
REQUIRE(sig_1->val_desc[0] == std::pair<double, std::string>{0, "disabled"});
REQUIRE(sig_1->val_desc[1] == std::pair<double, std::string>{1.2, "initializing"});
REQUIRE(sig_1->val_desc[2] == std::pair<double, std::string>{2, "fault"});
auto &sig_2 = msg->sigs[1];
REQUIRE(sig_2->comment == "multiple line comment \n1\n2");
@@ -147,7 +147,7 @@ TEST_CASE("parse_opendbc") {
QStringList errors;
for (auto fn : dir.entryList({"*.dbc"}, QDir::Files, QDir::Name)) {
try {
auto dbc = DBCFile(dir.filePath(fn));
auto dbc = DBCFile(dir.filePath(fn).toStdString());
} catch (std::exception &e) {
errors.push_back(e.what());
}

View File

@@ -1,10 +1,11 @@
#include "tools/cabana/tools/findsignal.h"
#include <thread>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QMenu>
#include <QtConcurrent>
#include <QTimer>
#include <QVBoxLayout>
@@ -20,7 +21,7 @@ QVariant FindSignalModel::data(const QModelIndex &index, int role) const {
if (role == Qt::DisplayRole) {
const auto &s = filtered_signals[index.row()];
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 2: return s.values.join(" ");
}
@@ -32,36 +33,49 @@ void FindSignalModel::search(std::function<bool(double)> cmp) {
beginResetModel();
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.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)); });
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});
}
});
unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency());
size_t chunk = (prev_sigs.size() + num_threads - 1) / num_threads;
std::vector<std::thread> threads;
for (unsigned int t = 0; t < num_threads && t * chunk < (size_t)prev_sigs.size(); ++t) {
size_t start = t * chunk;
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);
endResetModel();
}
void FindSignalModel::undo() {
if (!histories.isEmpty()) {
if (!histories.empty()) {
beginResetModel();
histories.pop_back();
filtered_signals.clear();
if (!histories.isEmpty()) filtered_signals = histories.back();
if (!histories.empty()) filtered_signals = histories.back();
endResetModel();
}
}
@@ -172,7 +186,7 @@ FindSignalDlg::FindSignalDlg(QWidget *parent) : QDialog(parent, Qt::WindowFlags(
}
void FindSignalDlg::search() {
if (model->histories.isEmpty()) {
if (model->histories.empty()) {
setInitialSignals();
}
auto v1 = value1->text().toDouble();
@@ -246,12 +260,12 @@ void FindSignalDlg::setInitialSignals() {
}
void FindSignalDlg::modelReset() {
properties_group->setEnabled(model->histories.isEmpty());
message_group->setEnabled(model->histories.isEmpty());
search_btn->setText(model->histories.isEmpty() ? tr("Find") : tr("Find Next"));
reset_btn->setEnabled(!model->histories.isEmpty());
properties_group->setEnabled(model->histories.empty());
message_group->setEnabled(model->histories.empty());
search_btn->setText(model->histories.empty() ? tr("Find") : tr("Find Next"));
reset_btn->setEnabled(!model->histories.empty());
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->setText(tr("%1 matches. right click on an item to create signal. double click to open message").arg(model->filtered_signals.size()));
}

View File

@@ -2,6 +2,8 @@
#include <algorithm>
#include <limits>
#include <string>
#include <vector>
#include <QAbstractTableModel>
#include <QCheckBox>
@@ -26,14 +28,14 @@ public:
QVariant headerData(int section, Qt::Orientation orientation, 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 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 reset();
void undo();
QList<SearchSignal> filtered_signals;
QList<SearchSignal> initial_signals;
QList<QList<SearchSignal>> histories;
std::vector<SearchSignal> filtered_signals;
std::vector<SearchSignal> initial_signals;
std::vector<std::vector<SearchSignal>> histories;
uint64_t last_time = std::numeric_limits<uint64_t>::max();
};

View File

@@ -1,6 +1,7 @@
#include "tools/cabana/tools/findsimilarbits.h"
#include <algorithm>
#include <unordered_map>
#include <QGridLayout>
#include <QHeaderView>
@@ -31,7 +32,7 @@ FindSimilarBitsDlg::FindSimilarBitsDlg(QWidget *parent) : QDialog(parent, Qt::Wi
msg_cb = new QComboBox(this);
// TODO: update when src_bus_combo changes
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->setCurrentIndex(0);
@@ -114,10 +115,10 @@ void FindSimilarBitsDlg::find() {
search_btn->setEnabled(true);
}
QList<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) {
QHash<uint32_t, QVector<uint32_t>> mismatches;
QHash<uint32_t, uint32_t> msg_count;
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) {
std::unordered_map<uint32_t, std::vector<uint32_t>> mismatches;
std::unordered_map<uint32_t, uint32_t> msg_count;
const auto &events = can->allEvents();
int bit_to_find = -1;
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());
for (auto it = mismatches.begin(); it != mismatches.end(); ++it) {
if (auto cnt = msg_count[it.key()]; cnt > min_msgs_cnt) {
auto &mismatched = it.value();
for (int i = 0; i < mismatched.size(); ++i) {
if (auto cnt = msg_count[it->first]; cnt > (uint32_t)min_msgs_cnt) {
auto &mismatched = it->second;
for (int i = 0; i < (int)mismatched.size(); ++i) {
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});
}
}
}

View File

@@ -1,5 +1,7 @@
#pragma once
#include <vector>
#include <QComboBox>
#include <QDialog>
#include <QLineEdit>
@@ -22,7 +24,7 @@ private:
uint32_t address, byte_idx, bit_idx, mismatches, total;
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);
void find();

View File

@@ -26,7 +26,7 @@ void exportSignalsToCSV(const QString &file_name, const MessageId &msg_id) {
QTextStream stream(&file);
stream << "time,addr,bus";
for (auto s : msg->sigs)
stream << "," << s->name;
stream << "," << s->name.c_str();
stream << "\n";
for (auto e : can->events(msg_id)) {

View File

@@ -17,8 +17,7 @@
#include <QSurfaceFormat>
#include <QFileInfo>
#include <QPainterPath>
#include <QTextStream>
#include <QtXml/QDomDocument>
#include <unordered_map>
#include "common/util.h"
// SegmentTree
@@ -278,7 +277,7 @@ QString signalToolTip(const cabana::Signal *sig) {
Start Bit: %2 Size: %3<br />
MSB: %4 LSB: %5<br />
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");
}
@@ -325,36 +324,49 @@ void initApp(int argc, char *argv[], bool disable_hidpi) {
setSurfaceFormat();
}
static QHash<QString, QByteArray> load_bootstrap_icons() {
QHash<QString, QByteArray> icons;
static std::unordered_map<std::string, std::string> load_bootstrap_icons() {
std::unordered_map<std::string, std::string> icons;
QFile f(":/bootstrap-icons.svg");
if (f.open(QIODevice::ReadOnly | QIODevice::Text)) {
QDomDocument xml;
xml.setContent(&f);
QDomNode n = xml.documentElement().firstChild();
while (!n.isNull()) {
QDomElement e = n.toElement();
if (!e.isNull() && e.hasAttribute("id")) {
QString svg_str;
QTextStream stream(&svg_str);
n.save(stream, 0);
svg_str.replace("<symbol", "<svg");
svg_str.replace("</symbol>", "</svg>");
icons[e.attribute("id")] = svg_str.toUtf8();
std::string content = f.readAll().toStdString();
const std::string sym_open = "<symbol ";
const std::string sym_close = "</symbol>";
const std::string id_attr = "id=\"";
size_t pos = 0;
while ((pos = content.find(sym_open, pos)) != std::string::npos) {
size_t end = content.find(sym_close, pos);
if (end == std::string::npos) break;
end += sym_close.size();
// 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;
}
QPixmap bootstrapPixmap(const QString &id) {
static QHash<QString, QByteArray> icons = load_bootstrap_icons();
static auto icons = load_bootstrap_icons();
QPixmap pixmap;
if (auto it = icons.find(id); it != icons.end()) {
pixmap.loadFromData(it.value(), "svg");
auto it = icons.find(id.toStdString());
if (it != icons.end()) {
pixmap.loadFromData((const uchar *)it->second.data(), it->second.size(), "svg");
}
return pixmap;
}

View File

@@ -1,6 +1,7 @@
#include "tools/cabana/videowidget.h"
#include <algorithm>
#include <thread>
#include <QAction>
#include <QActionGroup>
@@ -9,7 +10,6 @@
#include <QPainter>
#include <QStyleOptionSlider>
#include <QVBoxLayout>
#include <QtConcurrent>
#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) {
std::mutex mutex;
QtConcurrent::blockingMap(qlog->events.cbegin(), qlog->events.cend(), [this, &mutex](const Event &e) {
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;
const auto &events = qlog->events;
unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency());
size_t chunk = (events.size() + num_threads - 1) / num_threads;
std::vector<std::thread> threads;
for (unsigned int t = 0; t < num_threads && t * chunk < events.size(); ++t) {
size_t start = t * chunk;
size_t end = std::min(start + chunk, events.size());
threads.emplace_back([this, &mutex, &events, start, end]() {
for (size_t i = start; i < end; ++i) {
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();
}
@@ -384,9 +396,9 @@ QPixmap StreamCameraView::generateThumbnail(QPixmap thumb, double seconds) {
void StreamCameraView::drawScrubThumbnail(QPainter &p) {
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()) {
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());
p.drawPixmap(thumb_rect.topLeft(), scaled_thumb);
drawTime(p, thumb_rect, thumbnail_dispaly_time);
@@ -394,9 +406,9 @@ void StreamCameraView::drawScrubThumbnail(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()) {
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()));
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);

View File

@@ -1,5 +1,6 @@
#pragma once
#include <map>
#include <memory>
#include <set>
#include <string>
@@ -47,8 +48,8 @@ private:
void drawTime(QPainter &p, const QRect &rect, double seconds);
QPropertyAnimation *fade_animation;
QMap<uint64_t, QPixmap> big_thumbnails;
QMap<uint64_t, QPixmap> thumbnails;
std::map<uint64_t, QPixmap> big_thumbnails;
std::map<uint64_t, QPixmap> thumbnails;
double thumbnail_dispaly_time = -1;
friend class VideoWidget;
};

Some files were not shown because too many files have changed in this diff Show More