Compare commits

..

1 Commits

Author SHA1 Message Date
Jason Wen 783e3dad39 phase 1 2026-03-03 12:26:11 -05:00
248 changed files with 14406 additions and 8136 deletions
@@ -34,10 +34,10 @@ jobs:
echo "tinygrad_ref=$ref" >> $GITHUB_OUTPUT echo "tinygrad_ref=$ref" >> $GITHUB_OUTPUT
echo "tinygrad_ref is $ref" echo "tinygrad_ref is $ref"
- name: Checkout docs repo (sunnypilot-models, gh-pages) - name: Checkout docs repo (sunnypilot-docs, gh-pages)
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: sunnypilot/sunnypilot-models repository: sunnypilot/sunnypilot-docs
ref: gh-pages ref: gh-pages
path: docs path: docs
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }} ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
@@ -202,7 +202,7 @@ jobs:
- name: Checkout docs repo - name: Checkout docs repo
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: sunnypilot/sunnypilot-models repository: sunnypilot/sunnypilot-docs
ref: gh-pages ref: gh-pages
path: docs path: docs
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }} ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
@@ -119,7 +119,7 @@ jobs:
- name: Checkout docs repo - name: Checkout docs repo
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: sunnypilot/sunnypilot-models repository: sunnypilot/sunnypilot-docs
ref: gh-pages ref: gh-pages
path: docs path: docs
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }} ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
running-workflow-name: 'build __nightly' running-workflow-name: 'build __nightly'
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
check-regexp: ^((?!.*(build prebuilt|create badges).*).)*$ check-regexp: ^((?!.*(build prebuilt|create badges).*).)*$
- uses: actions/checkout@v4 - uses: actions/checkout@v6
with: with:
submodules: true submodules: true
fetch-depth: 0 fetch-depth: 0
+1
View File
@@ -72,6 +72,7 @@ jobs:
git add . git add .
- name: update car docs - name: update car docs
run: | run: |
scons -j$(nproc) --minimal opendbc_repo
python selfdrive/car/docs.py python selfdrive/car/docs.py
git add docs/CARS.md git add docs/CARS.md
- name: Create Pull Request - name: Create Pull Request
+1 -1
View File
@@ -181,7 +181,7 @@ jobs:
echo "${{ github.sha }}" > ref_commit echo "${{ github.sha }}" > ref_commit
git add . git add .
git commit -m "process-replay refs for ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit" git commit -m "process-replay refs for ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit"
git push origin process-replay --force git push origin process-replay
- name: Run regen - name: Run regen
if: false if: false
timeout-minutes: 4 timeout-minutes: 4
+26 -13
View File
@@ -13,13 +13,13 @@ venv/
a.out a.out
.hypothesis .hypothesis
.cache/ .cache/
bin/
/docs_site/
*.mp4 *.mp4
*.dylib *.dylib
*.DSYM *.DSYM
*.d *.d
*.pem
*.pyc *.pyc
*.pyo *.pyo
.*.swp .*.swp
@@ -39,13 +39,11 @@ bin/
*.mo *.mo
*_pyx.cpp *_pyx.cpp
*.stats *.stats
*.pkl
*.pkl*
config.json config.json
clcache
compile_commands.json compile_commands.json
compare_runtime*.html compare_runtime*.html
# build artifacts
selfdrive/pandad/pandad selfdrive/pandad/pandad
cereal/services.h cereal/services.h
cereal/gen cereal/gen
@@ -58,36 +56,51 @@ system/camerad/test/ae_gray_test
.coverage* .coverage*
coverage.xml coverage.xml
htmlcov htmlcov
pandaextra
.mypy_cache/
flycheck_*
cppcheck_report.txt
comma*.sh
selfdrive/modeld/models/*.pkl*
sunnypilot/modeld*/models/*.pkl
# openpilot log files # openpilot log files
*.bz2 *.bz2
*.zst *.zst
*.rlog
build/ build/
!**/.gitkeep !**/.gitkeep
poetry.toml
Pipfile
### VisualStudioCode ### ### VisualStudioCode ###
*.vsix
.history
.ionide
.vscode/* .vscode/*
.history/
!.vscode/settings.json !.vscode/settings.json
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
!.vscode/*.code-snippets !.vscode/*.code-snippets
# agents # Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
.claude/ .claude/
.context/ .context/
PLAN.md PLAN.md
TASK.md TASK.md
CLAUDE.md
SKILL.md
### JetBrains ### ### JetBrains ###
!.idea/customTargets.xml !.idea/customTargets.xml
-1
View File
@@ -1 +0,0 @@
3.12.13
Vendored
+4 -4
View File
@@ -167,7 +167,7 @@ node {
env.GIT_COMMIT = checkout(scm).GIT_COMMIT env.GIT_COMMIT = checkout(scm).GIT_COMMIT
def excludeBranches = ['__nightly', 'devel', 'devel-staging', 'release3', 'release3-staging', def excludeBranches = ['__nightly', 'devel', 'devel-staging', 'release3', 'release3-staging',
'release-tici', 'release-tizi', 'release-tizi-staging', 'release-mici-staging', 'testing-closet*', 'hotfix-*'] 'release-tici', 'release-tizi', 'release-tizi-staging', 'testing-closet*', 'hotfix-*']
def excludeRegex = excludeBranches.join('|').replaceAll('\\*', '.*') def excludeRegex = excludeBranches.join('|').replaceAll('\\*', '.*')
if (env.BRANCH_NAME != 'master' && !env.BRANCH_NAME.contains('__jenkins_loop_')) { if (env.BRANCH_NAME != 'master' && !env.BRANCH_NAME.contains('__jenkins_loop_')) {
@@ -179,7 +179,7 @@ node {
try { try {
if (env.BRANCH_NAME == 'devel-staging') { if (env.BRANCH_NAME == 'devel-staging') {
deviceStage("build release-tizi-staging", "tizi-needs-can", [], [ deviceStage("build release-tizi-staging", "tizi-needs-can", [], [
step("build release-tizi-staging", "RELEASE_BRANCH=release-tizi-staging $SOURCE_DIR/release/build_release.sh && git push -f origin release-tizi-staging:release-mici-staging"), step("build release-tizi-staging", "RELEASE_BRANCH=release-tizi-staging $SOURCE_DIR/release/build_release.sh"),
]) ])
} }
@@ -218,14 +218,14 @@ node {
'camerad OX03C10': { 'camerad OX03C10': {
deviceStage("OX03C10", "tizi-ox03c10", ["UNSAFE=1"], [ deviceStage("OX03C10", "tizi-ox03c10", ["UNSAFE=1"], [
step("build", "cd system/manager && ./build.py"), step("build", "cd system/manager && ./build.py"),
step("test pandad", "pytest selfdrive/pandad/tests/test_pandad.py"), step("test pandad", "pytest selfdrive/pandad/tests/test_pandad.py", [diffPaths: ["panda", "selfdrive/pandad/"]]),
step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 90]), step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 90]),
]) ])
}, },
'camerad OS04C10': { 'camerad OS04C10': {
deviceStage("OS04C10", "tici-os04c10", ["UNSAFE=1"], [ deviceStage("OS04C10", "tici-os04c10", ["UNSAFE=1"], [
step("build", "cd system/manager && ./build.py"), step("build", "cd system/manager && ./build.py"),
step("test pandad", "pytest selfdrive/pandad/tests/test_pandad.py"), step("test pandad", "pytest selfdrive/pandad/tests/test_pandad.py", [diffPaths: ["panda", "selfdrive/pandad/"]]),
step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 90]), step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 90]),
]) ])
}, },
+2 -10
View File
@@ -1,16 +1,8 @@
Version 0.11.1 (2026-04-08) Version 0.10.4 (2026-02-17)
======================== ========================
* New driver monitoring model
* Improved image processing pipeline for driver camera
Version 0.11.0 (2026-03-17)
========================
* New driving model #36798
* Fully trained using a learned simulator
* Improved longitudinal performance in Experimental mode
* Reduce comma four standby power usage by 77% to 52 mW
* Kia K7 2017 support thanks to royjr! * Kia K7 2017 support thanks to royjr!
* Lexus LS 2018 support thanks to Hacheoy! * Lexus LS 2018 support thanks to Hacheoy!
* Reduce comma four standby power usage by 77% to 52 mW
Version 0.10.3 (2025-12-17) Version 0.10.3 (2025-12-17)
======================== ========================
+39 -54
View File
@@ -4,11 +4,9 @@ import sys
import sysconfig import sysconfig
import platform import platform
import shlex import shlex
import importlib
import numpy as np import numpy as np
import SCons.Errors import SCons.Errors
from SCons.Defaults import _stripixes
SCons.Warnings.warningAsException(True) SCons.Warnings.warningAsException(True)
@@ -16,6 +14,9 @@ Decider('MD5-timestamp')
SetOption('num_jobs', max(1, int(os.cpu_count()/2))) SetOption('num_jobs', max(1, int(os.cpu_count()/2)))
AddOption('--asan', action='store_true', help='turn on ASAN')
AddOption('--ubsan', action='store_true', help='turn on UBSan')
AddOption('--mutation', action='store_true', help='generate mutation-ready code')
AddOption('--ccflags', action='store', type='string', default='', help='pass arbitrary flags over the command line') AddOption('--ccflags', action='store', type='string', default='', help='pass arbitrary flags over the command line')
AddOption('--verbose', action='store_true', default=False, help='show full build commands') AddOption('--verbose', action='store_true', default=False, help='show full build commands')
AddOption('--minimal', AddOption('--minimal',
@@ -37,46 +38,24 @@ assert arch in [
"Darwin", # macOS arm64 (x86 not supported) "Darwin", # macOS arm64 (x86 not supported)
] ]
pkg_names = ['bzip2', 'capnproto', 'eigen', 'ffmpeg', 'libjpeg', 'libyuv', 'ncurses', 'zeromq', 'zstd'] if arch != "larch64":
pkgs = [importlib.import_module(name) for name in pkg_names] import bzip2
import capnproto
import eigen
# ***** enforce a whitelist of system libraries ***** import ffmpeg as ffmpeg_pkg
# this prevents silently relying on a 3rd party package, import libjpeg
# e.g. apt-installed libusb. all libraries should either import libyuv
# be distributed with all Linux distros and macOS, or import ncurses
# vendored in commaai/dependencies. import openssl3
allowed_system_libs = { import python3_dev
"EGL", "GLESv2", "GL", "Qt5Charts", "Qt5Core", "Qt5Gui", "Qt5Widgets", import zeromq
"dl", "drm", "gbm", "m", "pthread", import zstd
} pkgs = [bzip2, capnproto, eigen, ffmpeg_pkg, libjpeg, libyuv, ncurses, openssl3, zeromq, zstd]
py_include = python3_dev.INCLUDE_DIR
def _resolve_lib(env, name): else:
for d in env.Flatten(env.get('LIBPATH', [])): # TODO: remove when AGNOS has our new vendor pkgs
p = Dir(str(d)).abspath pkgs = []
for ext in ('.a', '.so', '.dylib'): py_include = sysconfig.get_paths()['include']
f = File(os.path.join(p, f'lib{name}{ext}'))
if f.exists() or f.has_builder():
return name
if name in allowed_system_libs:
return name
raise SCons.Errors.UserError(f"Unexpected non-vendored library '{name}'")
def _libflags(target, source, env, for_signature):
libs = []
lp = env.subst('$LIBLITERALPREFIX')
for lib in env.Flatten(env.get('LIBS', [])):
if isinstance(lib, str):
if os.sep in lib or lib.startswith('#'):
libs.append(File(lib))
elif lib.startswith('-') or (lp and lib.startswith(lp)):
libs.append(lib)
else:
libs.append(_resolve_lib(env, lib))
else:
libs.append(lib)
return _stripixes(env['LIBLINKPREFIX'], libs, env['LIBLINKSUFFIX'],
env['LIBPREFIXES'], env['LIBSUFFIXES'], env, env['LIBLITERALPREFIX'])
env = Environment( env = Environment(
ENV={ ENV={
@@ -129,14 +108,14 @@ env = Environment(
tools=["default", "cython", "compilation_db", "rednose_filter"], tools=["default", "cython", "compilation_db", "rednose_filter"],
toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"], toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"],
) )
if arch != "larch64":
env['_LIBFLAGS'] = _libflags
# Arch-specific flags and paths # Arch-specific flags and paths
if arch == "larch64": if arch == "larch64":
env["CC"] = "clang" env["CC"] = "clang"
env["CXX"] = "clang++" env["CXX"] = "clang++"
env.Append(LIBPATH=[ env.Append(LIBPATH=[
"/usr/local/lib",
"/system/vendor/lib64",
"/usr/lib/aarch64-linux-gnu", "/usr/lib/aarch64-linux-gnu",
]) ])
arch_flags = ["-D__TICI__", "-mcpu=cortex-a57", "-DQCOM2"] arch_flags = ["-D__TICI__", "-mcpu=cortex-a57", "-DQCOM2"]
@@ -148,6 +127,19 @@ elif arch == "Darwin":
]) ])
env.Append(CCFLAGS=["-DGL_SILENCE_DEPRECATION"]) env.Append(CCFLAGS=["-DGL_SILENCE_DEPRECATION"])
env.Append(CXXFLAGS=["-DGL_SILENCE_DEPRECATION"]) env.Append(CXXFLAGS=["-DGL_SILENCE_DEPRECATION"])
else:
env.Append(LIBPATH=[
"/usr/lib",
"/usr/local/lib",
])
# Sanitizers and extra CCFLAGS from CLI
if GetOption('asan'):
env.Append(CCFLAGS=["-fsanitize=address", "-fno-omit-frame-pointer"])
env.Append(LINKFLAGS=["-fsanitize=address"])
elif GetOption('ubsan'):
env.Append(CCFLAGS=["-fsanitize=undefined"])
env.Append(LINKFLAGS=["-fsanitize=undefined"])
_extra_cc = shlex.split(GetOption('ccflags') or '') _extra_cc = shlex.split(GetOption('ccflags') or '')
if _extra_cc: if _extra_cc:
@@ -185,7 +177,7 @@ if os.environ.get('SCONS_PROGRESS'):
# ********** Cython build environment ********** # ********** Cython build environment **********
envCython = env.Clone() envCython = env.Clone()
envCython["CPPPATH"] += [sysconfig.get_paths()['include'], np.get_include()] envCython["CPPPATH"] += [py_include, np.get_include()]
envCython["CCFLAGS"] += ["-Wno-#warnings", "-Wno-cpp", "-Wno-shadow", "-Wno-deprecated-declarations"] envCython["CCFLAGS"] += ["-Wno-#warnings", "-Wno-cpp", "-Wno-shadow", "-Wno-deprecated-declarations"]
envCython["CCFLAGS"].remove("-Werror") envCython["CCFLAGS"].remove("-Werror")
@@ -219,6 +211,7 @@ Export('common')
env_swaglog = env.Clone() env_swaglog = env.Clone()
env_swaglog['CXXFLAGS'].append('-DSWAGLOG="\\"common/swaglog.h\\""') env_swaglog['CXXFLAGS'].append('-DSWAGLOG="\\"common/swaglog.h\\""')
SConscript(['msgq_repo/SConscript'], exports={'env': env_swaglog}) SConscript(['msgq_repo/SConscript'], exports={'env': env_swaglog})
SConscript(['opendbc_repo/SConscript'], exports={'env': env_swaglog})
SConscript(['cereal/SConscript']) SConscript(['cereal/SConscript'])
@@ -244,15 +237,7 @@ if arch == "larch64":
# Build openpilot # Build openpilot
SConscript(['third_party/SConscript']) SConscript(['third_party/SConscript'])
# Build selfdrive SConscript(['selfdrive/SConscript'])
SConscript([
'selfdrive/pandad/SConscript',
'selfdrive/controls/lib/lateral_mpc_lib/SConscript',
'selfdrive/controls/lib/longitudinal_mpc_lib/SConscript',
'selfdrive/locationd/SConscript',
'selfdrive/modeld/SConscript',
'selfdrive/ui/SConscript',
])
SConscript(['sunnypilot/SConscript']) SConscript(['sunnypilot/SConscript'])
+1
View File
@@ -0,0 +1 @@
*.cpp
+1 -1
View File
@@ -1,4 +1,4 @@
Import('env', 'envCython') Import('env', 'envCython', 'arch')
common_libs = [ common_libs = [
'params.cc', 'params.cc',
+1 -1
View File
@@ -28,7 +28,7 @@ class BounceFilter(FirstOrderFilter):
scale = self.dt / (1.0 / 60.0) # tuned at 60 fps scale = self.dt / (1.0 / 60.0) # tuned at 60 fps
self.velocity.x += (x - self.x) * self.bounce * scale * self.dt self.velocity.x += (x - self.x) * self.bounce * scale * self.dt
self.velocity.update(0.0) self.velocity.update(0.0)
if abs(self.velocity.x) < 1e-3: if abs(self.velocity.x) < 1e-5:
self.velocity.x = 0.0 self.velocity.x = 0.0
self.x += self.velocity.x self.x += self.velocity.x
return self.x return self.x
+1 -2
View File
@@ -172,7 +172,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"OnroadScreenOffBrightness", {PERSISTENT | BACKUP, INT, "0"}}, {"OnroadScreenOffBrightness", {PERSISTENT | BACKUP, INT, "0"}},
{"OnroadScreenOffBrightnessMigrated", {PERSISTENT | BACKUP, STRING, "0.0"}}, {"OnroadScreenOffBrightnessMigrated", {PERSISTENT | BACKUP, STRING, "0.0"}},
{"OnroadScreenOffTimer", {PERSISTENT | BACKUP, INT, "15"}}, {"OnroadScreenOffTimer", {PERSISTENT | BACKUP, INT, "15"}},
{"OnroadScreenOffTimerMigrated", {PERSISTENT | BACKUP, STRING, "0.0"}},
{"OnroadUploads", {PERSISTENT | BACKUP, BOOL, "1"}}, {"OnroadUploads", {PERSISTENT | BACKUP, BOOL, "1"}},
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}}, {"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}}, {"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -271,7 +270,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"EnforceTorqueControl", {PERSISTENT | BACKUP, BOOL}}, {"EnforceTorqueControl", {PERSISTENT | BACKUP, BOOL}},
{"LiveTorqueParamsToggle", {PERSISTENT | BACKUP , BOOL}}, {"LiveTorqueParamsToggle", {PERSISTENT | BACKUP , BOOL}},
{"LiveTorqueParamsRelaxedToggle", {PERSISTENT | BACKUP , BOOL}}, {"LiveTorqueParamsRelaxedToggle", {PERSISTENT | BACKUP , BOOL}},
{"TorqueControlTune", {PERSISTENT | BACKUP, FLOAT, "0.0"}}, {"TorqueControlTune", {PERSISTENT | BACKUP, FLOAT}},
{"TorqueParamsOverrideEnabled", {PERSISTENT | BACKUP, BOOL, "0"}}, {"TorqueParamsOverrideEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TorqueParamsOverrideFriction", {PERSISTENT | BACKUP, FLOAT, "0.1"}}, {"TorqueParamsOverrideFriction", {PERSISTENT | BACKUP, FLOAT, "0.1"}},
{"TorqueParamsOverrideLatAccelFactor", {PERSISTENT | BACKUP, FLOAT, "2.5"}}, {"TorqueParamsOverrideLatAccelFactor", {PERSISTENT | BACKUP, FLOAT, "2.5"}},
+1 -2
View File
@@ -2,7 +2,6 @@ import datetime
from pathlib import Path from pathlib import Path
MIN_DATE = datetime.datetime(year=2025, month=2, day=21) MIN_DATE = datetime.datetime(year=2025, month=2, day=21)
MAX_DATE = datetime.datetime(year=2035, month=1, day=1)
def min_date(): def min_date():
# on systemd systems, the default time is the systemd build time # on systemd systems, the default time is the systemd build time
@@ -13,4 +12,4 @@ def min_date():
return MIN_DATE return MIN_DATE
def system_time_valid(): def system_time_valid():
return min_date() < datetime.datetime.now() < MAX_DATE return datetime.datetime.now() > min_date()
+2
View File
@@ -0,0 +1,2 @@
transformations
transformations.cpp
+1 -4
View File
@@ -65,10 +65,7 @@ DEVICE_CAMERAS = {
("unknown", "ox03c10"): _ar_ox_config, ("unknown", "ox03c10"): _ar_ox_config,
# simulator (emulates a tici) # simulator (emulates a tici)
("pc", "unknown"): _os_config, ("pc", "unknown"): _ar_ox_config,
# ("pc", "ar0231"): _ar_ox_config,
# ("pc", "ox03c10"): _ar_ox_config,
# ("pc", "os04c10"): _os_config,
} }
prods = itertools.product(('tici', 'tizi', 'mici'), (('ar0231', _ar_ox_config), ('ox03c10', _ar_ox_config), ('os04c10', _os_config))) prods = itertools.product(('tici', 'tizi', 'mici'), (('ar0231', _ar_ox_config), ('ox03c10', _ar_ox_config), ('os04c10', _os_config)))
DEVICE_CAMERAS.update({(d, c[0]): c[1] for d, c in prods}) DEVICE_CAMERAS.update({(d, c[0]): c[1] for d, c in prods})
+1 -1
View File
@@ -1 +1 @@
#define COMMA_VERSION "0.11.1" #define COMMA_VERSION "0.10.4"
+1
View File
@@ -10,6 +10,7 @@ from openpilot.system.hardware import TICI, HARDWARE
# TODO: pytest-cpp doesn't support FAIL, and we need to create test translations in sessionstart # TODO: pytest-cpp doesn't support FAIL, and we need to create test translations in sessionstart
# pending https://github.com/pytest-dev/pytest-cpp/pull/147 # pending https://github.com/pytest-dev/pytest-cpp/pull/147
collect_ignore = [ collect_ignore = [
"selfdrive/ui/tests/test_translations",
"selfdrive/test/process_replay/test_processes.py", "selfdrive/test/process_replay/test_processes.py",
"selfdrive/test/process_replay/test_regen.py", "selfdrive/test/process_replay/test_regen.py",
] ]
+1 -1
View File
@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
export QCOM_PRIORITY=12 export QCOM_PRIORITY=12
if [ -z "$AGNOS_VERSION" ]; then if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="17.2" export AGNOS_VERSION="16"
fi fi
export STAGING_ROOT="/data/safe_staging" export STAGING_ROOT="/data/safe_staging"
+1 -1
Submodule panda updated: 6ddc631bdd...f5f296c65c
+14 -17
View File
@@ -26,18 +26,18 @@ dependencies = [
"numpy >=2.0", "numpy >=2.0",
# vendored native dependencies # vendored native dependencies
"bzip2 @ git+https://github.com/commaai/dependencies.git@release-bzip2#subdirectory=bzip2", "bzip2 @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=bzip2",
"capnproto @ git+https://github.com/commaai/dependencies.git@release-capnproto#subdirectory=capnproto", "capnproto @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=capnproto",
"eigen @ git+https://github.com/commaai/dependencies.git@release-eigen#subdirectory=eigen", "eigen @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=eigen",
"ffmpeg @ git+https://github.com/commaai/dependencies.git@release-ffmpeg#subdirectory=ffmpeg", "ffmpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ffmpeg",
"libjpeg @ git+https://github.com/commaai/dependencies.git@release-libjpeg#subdirectory=libjpeg", "libjpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=libjpeg",
"libyuv @ git+https://github.com/commaai/dependencies.git@release-libyuv#subdirectory=libyuv", "libyuv @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=libyuv",
"zstd @ git+https://github.com/commaai/dependencies.git@release-zstd#subdirectory=zstd", "openssl3 @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=openssl3",
"ncurses @ git+https://github.com/commaai/dependencies.git@release-ncurses#subdirectory=ncurses", "python3-dev @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=python3-dev",
"zeromq @ git+https://github.com/commaai/dependencies.git@release-zeromq#subdirectory=zeromq", "zstd @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zstd",
"libusb @ git+https://github.com/commaai/dependencies.git@release-libusb#subdirectory=libusb", "ncurses @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ncurses",
"git-lfs @ git+https://github.com/commaai/dependencies.git@release-git-lfs#subdirectory=git-lfs", "zeromq @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zeromq",
"gcc-arm-none-eabi @ git+https://github.com/commaai/dependencies.git@release-gcc-arm-none-eabi#subdirectory=gcc-arm-none-eabi", "git-lfs @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=git-lfs",
# body / webrtcd # body / webrtcd
"av", "av",
@@ -76,7 +76,6 @@ dependencies = [
"raylib > 5.5.0.3", "raylib > 5.5.0.3",
"qrcode", "qrcode",
"jeepney", "jeepney",
"pillow",
] ]
[project.optional-dependencies] [project.optional-dependencies]
@@ -104,10 +103,12 @@ testing = [
dev = [ dev = [
"matplotlib", "matplotlib",
"opencv-python-headless", "opencv-python-headless",
"gcc-arm-none-eabi @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=gcc-arm-none-eabi",
] ]
tools = [ tools = [
"metadrive-simulator @ git+https://github.com/commaai/metadrive.git@minimal ; (platform_machine != 'aarch64')", "metadrive-simulator @ git+https://github.com/commaai/metadrive.git@minimal ; (platform_machine != 'aarch64')",
"dearpygui>=2.1.0; (sys_platform != 'linux' or platform_machine != 'aarch64')", # not vended for linux aarch64
] ]
[project.urls] [project.urls]
@@ -206,7 +207,6 @@ lint.flake8-implicit-str-concat.allow-multiline = false
"pyray.is_mouse_button_pressed".msg = "This can miss events. Use Widget._handle_mouse_press" "pyray.is_mouse_button_pressed".msg = "This can miss events. Use Widget._handle_mouse_press"
"pyray.is_mouse_button_released".msg = "This can miss events. Use Widget._handle_mouse_release" "pyray.is_mouse_button_released".msg = "This can miss events. Use Widget._handle_mouse_release"
"pyray.draw_text".msg = "Use a function (such as rl.draw_font_ex) that takes font as an argument" "pyray.draw_text".msg = "Use a function (such as rl.draw_font_ex) that takes font as an argument"
"pyray.draw_texture".msg = "Use rl.draw_texture_ex for float position support"
[tool.ruff.format] [tool.ruff.format]
quote-style = "preserve" quote-style = "preserve"
@@ -250,6 +250,3 @@ unsupported-operator = "ignore"
# Ignore not-subscriptable - false positives from dynamic types # Ignore not-subscriptable - false positives from dynamic types
not-subscriptable = "ignore" not-subscriptable = "ignore"
# not-iterable errors are now fixed # not-iterable errors are now fixed
[tool.uv]
python-preference = "only-managed"
+2 -5
View File
@@ -12,13 +12,12 @@ from openpilot.common.basedir import BASEDIR
DIRS = ['cereal', 'openpilot'] DIRS = ['cereal', 'openpilot']
EXTS = ['.png', '.py', '.ttf', '.capnp', '.json', '.fnt', '.mo', '.po'] EXTS = ['.png', '.py', '.ttf', '.capnp', '.json', '.fnt', '.mo']
EXCLUDE = ['selfdrive/assets/training', 'third_party/raylib/raylib_repo/examples']
INTERPRETER = '/usr/bin/env python3' INTERPRETER = '/usr/bin/env python3'
def copy(src, dest): def copy(src, dest):
if any(src.endswith(ext) for ext in EXTS) and not any(exc in src for exc in EXCLUDE): if any(src.endswith(ext) for ext in EXTS):
shutil.copy2(src, dest, follow_symlinks=True) shutil.copy2(src, dest, follow_symlinks=True)
@@ -29,8 +28,6 @@ if __name__ == '__main__':
parser.add_argument('module', help="the module to target, e.g. 'openpilot.system.ui.spinner'") parser.add_argument('module', help="the module to target, e.g. 'openpilot.system.ui.spinner'")
args = parser.parse_args() args = parser.parse_args()
print('WARNING: copying all files! make sure to run scons and git tree is clean')
if not args.output: if not args.output:
args.output = args.module args.output = args.module
+6
View File
@@ -0,0 +1,6 @@
SConscript(['pandad/SConscript'])
SConscript(['controls/lib/lateral_mpc_lib/SConscript'])
SConscript(['controls/lib/longitudinal_mpc_lib/SConscript'])
SConscript(['locationd/SConscript'])
SConscript(['modeld/SConscript'])
SConscript(['ui/SConscript'])
+2
View File
@@ -1,2 +1,4 @@
*.cc
fonts/*.fnt fonts/*.fnt
fonts/*.png fonts/*.png
translations_assets.qrc
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f3f57346a1cf9a66f9fd746f87bcebb23b7a403e9d6e4fd7701b126abcdd47ea
size 18476
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f646263b26de46f79cac836ef6865b0f25ddc91e386b99311723b68bd06693c9
size 3304
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d29a9c295b33b3164c37a68ad77795595e6ac877a5b308d28112b0315ecd498f
size 1687
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a6892bd4d9b14b587fa491a6d608562e38819b4c618b1d7a3e8c384f05d52a2b
size 1245
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3428d8fcf2ecf9542c524706124f82b7fc809453c63418c9234ac9df5d85bd24
size 10074
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b2810add4943dd4f20a984ed6011b520925919a58d5c0dd0d846fc4d7f8a1d02
size 7109
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a3a87454a3d2f1ebb327211062c52480de945673dcfd137c5da3df8fa98d731
size 22400
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:122a614d1aa26187507951f932160eebfddfebcb4293e78f8d23e350fc97bc0f
size 11489
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9e363a79dc35ca4c4e9efaa6a843d37ad219efa5299d3e538d8249affa230096
size 7935
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cc6fb48520143b6fa1f060d8212e6d929917ab616ce943b5fab5a60665f00da5
size 18225
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7a198f13f30b3dbc09f30d7fd8033a0bc07a0da9b010b7ca6ed2678430c9e5b4
size 6949
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:75289d004709def2a2d6101a0330ec867895068ec3807aefc2a26d423d907a13
size 13437
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2452aaf59da18be1b74b475851d66e5c73c50aa49820419a288b1fdb7b42dee1
size 9071
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6478f7c1c5ef2013e94fc4218ab370889883c5c12231ba3e0975874cb0b6fec9
size 21893
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8d5b8f76e5f47e77e5af3016ebdbe548ad3bc9af83a1111b3214bf4017c95a28
size 11792
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:63c1499106621a4d927c21b2b04c87235a927216d9f513a0205f0fe03b8c799b oid sha256:1f5ee67cd334d259ac33f932281db36533877009b5769c92d9cff3054fd5627c
size 12320 size 2942
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a733c425113a7f6ff5ec3dc50ef94b5481c0f2d306e33d1485be8ee6b2798532
size 1136
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b3a336afddad80dc91caca91d54bd29897ce491f180374edf9a5ba517cbc00e9
size 8765
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8eee9f10ca80a4e6100c00c02bb46aa5f253b14b086ab9982cfa85ee94eec162
size 22512
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:94a86fac6ffe8a8179812cf55350ab9ca6935f36244c6f679c1cf521a842316b
size 5723
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6ccb5f2298389ae36df87de84d85440ee5a82c50e803c9bd362c9b89ea45aa69
size 6611
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a804da77b268f0a625f93949642ae74cdfe5b5caa5baea1c52c4605ae25c80e4
size 12916
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:89ca7e6bb01dfa78300126ce828cb2a64e7a2e68e1e9152de242f57a36d0e57a
size 8604
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b3242a411b559f1d0308f189fe0d25b81d6c7d964ca418a0c599a1bab4bffcbb
size 5341
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d354651c0c8107dcc5f599777d260f53ef1901123315785ed8190466166cdce8
size 17554
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:74fc21132b1e761ea54ce64617730c6ee79d01668244ab555b3b89870cfea181
size 7112
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9728423bd5e3197ef02d62e4bae415e6694aab875ca8630ffc9f188c38e18e5f
size 4141
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0ff179f93f421edcb503ca5c22a12b37e3a2aaabc414bf90f57e20ff5255dd75
size 15572
+1
View File
@@ -0,0 +1 @@
*.bz2
+2
View File
@@ -0,0 +1,2 @@
calibration_param
traces
+2
View File
@@ -0,0 +1,2 @@
params_learner
paramsd
+10 -28
View File
@@ -29,26 +29,11 @@ MIN_LAG = 0.15
MAX_LAG_STD = 0.1 MAX_LAG_STD = 0.1
MAX_LAT_ACCEL = 2.0 MAX_LAT_ACCEL = 2.0
MAX_LAT_ACCEL_DIFF = 0.6 MAX_LAT_ACCEL_DIFF = 0.6
MIN_LAT_ACCEL_RANGE = 0.5
MIN_CONFIDENCE = 0.7 MIN_CONFIDENCE = 0.7
CORR_BORDER_OFFSET = 5 CORR_BORDER_OFFSET = 5
LAG_CANDIDATE_CORR_THRESHOLD = 0.9 LAG_CANDIDATE_CORR_THRESHOLD = 0.9
SMOOTH_K = 5
SMOOTH_SIGMA = 1.0
def masked_symmetric_moving_average(x: np.ndarray, mask: np.ndarray, k: int, sigma: float) -> np.ndarray:
assert k >= 1 and k % 2 == 1, "k must be positive and odd"
pad = k // 2
i = np.arange(k) - pad
w = np.exp(-0.5 * (i / sigma) ** 2)
w /= w.sum()
xp = np.pad(x * mask, pad, mode="edge")
mp = np.pad(mask, pad, mode="edge")
num = np.convolve(xp, w, mode="valid")
den = np.convolve(mp, w, mode="valid")
return np.divide(num, den, out=np.full_like(num, np.nan, dtype=np.float64), where=den != 0)
def masked_normalized_cross_correlation(expected_sig: np.ndarray, actual_sig: np.ndarray, mask: np.ndarray, n: int): def masked_normalized_cross_correlation(expected_sig: np.ndarray, actual_sig: np.ndarray, mask: np.ndarray, n: int):
""" """
References: References:
@@ -310,14 +295,11 @@ class LateralLagEstimator:
times, desired, actual, okay = self.points.get() times, desired, actual, okay = self.points.get()
# check if there are any new valid data points since the last update # check if there are any new valid data points since the last update
is_valid = self.points_valid() and (actual.max() - actual.min() >= MIN_LAT_ACCEL_RANGE) is_valid = self.points_valid()
if self.last_estimate_t != 0 and times[0] <= self.last_estimate_t: if self.last_estimate_t != 0 and times[0] <= self.last_estimate_t:
new_values_start_idx = next(-i for i, t in enumerate(reversed(times)) if t <= self.last_estimate_t) new_values_start_idx = next(-i for i, t in enumerate(reversed(times)) if t <= self.last_estimate_t)
is_valid = is_valid and not (new_values_start_idx == 0 or not np.any(okay[new_values_start_idx:])) is_valid = is_valid and not (new_values_start_idx == 0 or not np.any(okay[new_values_start_idx:]))
desired = masked_symmetric_moving_average(desired, okay, SMOOTH_K, SMOOTH_SIGMA)
actual = masked_symmetric_moving_average(actual, okay, SMOOTH_K, SMOOTH_SIGMA)
delay, corr, confidence = self.actuator_delay(desired, actual, okay, self.dt, MIN_LAG, MAX_LAG) delay, corr, confidence = self.actuator_delay(desired, actual, okay, self.dt, MIN_LAG, MAX_LAG)
if corr < self.min_ncc or confidence < self.min_confidence or not is_valid: if corr < self.min_ncc or confidence < self.min_confidence or not is_valid:
return return
@@ -329,16 +311,16 @@ class LateralLagEstimator:
def actuator_delay(expected_sig: np.ndarray, actual_sig: np.ndarray, mask: np.ndarray, def actuator_delay(expected_sig: np.ndarray, actual_sig: np.ndarray, mask: np.ndarray,
dt: float, min_lag: float, max_lag: float) -> tuple[float, float, float]: dt: float, min_lag: float, max_lag: float) -> tuple[float, float, float]:
assert len(expected_sig) == len(actual_sig) assert len(expected_sig) == len(actual_sig)
min_lag_samples, max_lag_samples, one_sec_samples = int(round(min_lag / dt)), int(round(max_lag / dt)), int(round(1.0 / dt)) min_lag_samples, max_lag_samples = int(round(min_lag / dt)), int(round(max_lag / dt))
padded_size = fft_next_good_size(len(expected_sig) + max(max_lag_samples, one_sec_samples)) padded_size = fft_next_good_size(len(expected_sig) + max_lag_samples)
ncc = masked_normalized_cross_correlation(expected_sig, actual_sig, mask, padded_size) ncc = masked_normalized_cross_correlation(expected_sig, actual_sig, mask, padded_size)
# only consider lags from ranges: # only consider lags from min_lag to max_lag
roi = np.s_[len(expected_sig) - 1 + min_lag_samples: len(expected_sig) - 1 + max_lag_samples] # min_lag - max_lag range roi = np.s_[len(expected_sig) - 1 + min_lag_samples: len(expected_sig) - 1 + max_lag_samples]
threshold_roi = np.s_[len(expected_sig) - 1: len(expected_sig) - 1 + one_sec_samples] # 0 - 1 second range extended_roi = np.s_[roi.start - CORR_BORDER_OFFSET: roi.stop + CORR_BORDER_OFFSET]
confidence_roi = np.s_[threshold_roi.start - CORR_BORDER_OFFSET: threshold_roi.stop + CORR_BORDER_OFFSET] # threshold range +/- border roi_ncc = ncc[roi]
roi_ncc, confidence_roi_ncc, threshold_roi_ncc = ncc[roi], ncc[confidence_roi], ncc[threshold_roi] extended_roi_ncc = ncc[extended_roi]
max_corr_index = np.argmax(roi_ncc) max_corr_index = np.argmax(roi_ncc)
corr = roi_ncc[max_corr_index] corr = roi_ncc[max_corr_index]
@@ -346,8 +328,8 @@ class LateralLagEstimator:
# to estimate lag confidence, gather all high-correlation candidates and see how spread they are # to estimate lag confidence, gather all high-correlation candidates and see how spread they are
# if e.g. 0.8 and 0.4 are both viable, this is an ambiguous case # if e.g. 0.8 and 0.4 are both viable, this is an ambiguous case
ncc_thresh = (threshold_roi_ncc.max() - threshold_roi_ncc.min()) * LAG_CANDIDATE_CORR_THRESHOLD + threshold_roi_ncc.min() ncc_thresh = (roi_ncc.max() - roi_ncc.min()) * LAG_CANDIDATE_CORR_THRESHOLD + roi_ncc.min()
good_lag_candidate_mask = confidence_roi_ncc >= ncc_thresh good_lag_candidate_mask = extended_roi_ncc >= ncc_thresh
good_lag_candidate_edges = np.diff(good_lag_candidate_mask.astype(int), prepend=0, append=0) good_lag_candidate_edges = np.diff(good_lag_candidate_mask.astype(int), prepend=0, append=0)
starts, ends = np.where(good_lag_candidate_edges == 1)[0], np.where(good_lag_candidate_edges == -1)[0] - 1 starts, ends = np.where(good_lag_candidate_edges == 1)[0], np.where(good_lag_candidate_edges == -1)[0] - 1
run_idx = np.searchsorted(starts, max_corr_index + CORR_BORDER_OFFSET, side='right') - 1 run_idx = np.searchsorted(starts, max_corr_index + CORR_BORDER_OFFSET, side='right') - 1
+1
View File
@@ -0,0 +1 @@
out/
+2 -2
View File
@@ -19,8 +19,8 @@ DT = 0.05
def process_messages(estimator, lag_frames, n_frames, vego=20.0, rejection_threshold=0.0): def process_messages(estimator, lag_frames, n_frames, vego=20.0, rejection_threshold=0.0):
for i in range(n_frames): for i in range(n_frames):
t = i * estimator.dt t = i * estimator.dt
desired_la = np.cos(10 * t) * 0.3 desired_la = np.cos(10 * t) * 0.1
actual_la = np.cos(10 * (t - lag_frames * estimator.dt)) * 0.3 actual_la = np.cos(10 * (t - lag_frames * estimator.dt)) * 0.1
# if sample is masked out, set it to desired value (no lag) # if sample is masked out, set it to desired value (no lag)
rejected = random.uniform(0, 1) < rejection_threshold rejected = random.uniform(0, 1) < rejection_threshold
+3 -7
View File
@@ -45,17 +45,13 @@ def tg_compile(flags, model_name):
pkl = fn + "_tinygrad.pkl" pkl = fn + "_tinygrad.pkl"
onnx_path = fn + ".onnx" onnx_path = fn + ".onnx"
chunk_targets = get_chunk_paths(pkl, estimate_pickle_max_size(os.path.getsize(onnx_path))) chunk_targets = get_chunk_paths(pkl, estimate_pickle_max_size(os.path.getsize(onnx_path)))
compile_node = lenv.Command(
pkl,
[onnx_path] + tinygrad_files + [chunker_file],
f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {pkl}',
)
def do_chunk(target, source, env): def do_chunk(target, source, env):
chunk_file(pkl, chunk_targets) chunk_file(pkl, chunk_targets)
return lenv.Command( return lenv.Command(
chunk_targets, chunk_targets,
compile_node, [onnx_path] + tinygrad_files + [chunker_file],
do_chunk, [f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {pkl}',
do_chunk]
) )
# Compile small models # Compile small models
+16 -22
View File
@@ -32,7 +32,8 @@ def flash_panda(panda_serial: str) -> Panda:
raise raise
# skip flashing if the detected panda is not supported # skip flashing if the detected panda is not supported
if panda.get_type() not in Panda.SUPPORTED_DEVICES: supported_panda = check_panda_support(panda)
if not supported_panda:
cloudlog.warning(f"Panda {panda_serial} is not supported (hw_type: {panda.get_type()}), skipping flash...") cloudlog.warning(f"Panda {panda_serial} is not supported (hw_type: {panda.get_type()}), skipping flash...")
return panda return panda
@@ -68,20 +69,12 @@ def flash_panda(panda_serial: str) -> Panda:
return panda return panda
def check_panda_support(panda_serials: list[str]) -> list[str]: def check_panda_support(panda) -> bool:
spi_serials = set(Panda.spi_list()) hw_type = panda.get_type()
for serial in panda_serials: if hw_type in Panda.SUPPORTED_DEVICES:
if serial in spi_serials: return True
return [serial]
for serial in panda_serials: return False
panda = Panda(serial)
is_internal = panda.is_internal()
panda.close()
if is_internal:
return [serial]
return []
def main() -> None: def main() -> None:
@@ -133,18 +126,13 @@ def main() -> None:
cloudlog.info(f"{len(panda_serials)} panda(s) found, connecting - {panda_serials}") cloudlog.info(f"{len(panda_serials)} panda(s) found, connecting - {panda_serials}")
# custom flasher for xnor's Rivian Longitudinal Upgrade Kit
flash_rivian_long(panda_serials)
# 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 # Flash the first panda
panda_serial = panda_serials[0] panda_serial = panda_serials[0]
panda = flash_panda(panda_serial) panda = flash_panda(panda_serial)
# flash Rivian longitudinal upgrade panda
flash_rivian_long(panda)
# Ensure internal panda is present if expected # Ensure internal panda is present if expected
if HARDWARE.has_internal_panda() and not panda.is_internal(): if HARDWARE.has_internal_panda() and not panda.is_internal():
cloudlog.error("Internal panda is missing, trying again") cloudlog.error("Internal panda is missing, trying again")
@@ -155,6 +143,12 @@ def main() -> None:
# log panda fw version # log panda fw version
params.put("PandaSignatures", panda.get_signature()) params.put("PandaSignatures", panda.get_signature())
# skip health check if the detected panda is not supported
supported_panda = check_panda_support(panda)
if not supported_panda:
cloudlog.warning(f"Panda {panda.get_usb_serial()} is not supported (hw_type: {panda.get_type()}), skipping health check...")
continue
# check health for lost heartbeat # check health for lost heartbeat
health = panda.health() health = panda.health()
if health["heartbeat_lost"]: if health["heartbeat_lost"]:
+1 -1
View File
@@ -3,7 +3,7 @@ docker_out/
process_replay/diff.txt process_replay/diff.txt
process_replay/model_diff.txt process_replay/model_diff.txt
process_replay/fakedata/
valgrind_logs.txt valgrind_logs.txt
*.bz2
*.hevc *.hevc
+1
View File
@@ -0,0 +1 @@
fakedata/
+1 -6
View File
@@ -342,15 +342,10 @@ class TestOnroad:
start, end = min(first_fid), min(last_fid) start, end = min(first_fid), min(last_fid)
for i in range(end-start): for i in range(end-start):
# road and wide cameras (first two) should be synced within 2ms ts = {c: round(self.ts[c]['timestampSof'][i]/1e6, 1) for c in cams}
ts = {c: round(self.ts[c]['timestampSof'][i]/1e6, 1) for c in cams[:2]}
diff = (max(ts.values()) - min(ts.values())) diff = (max(ts.values()) - min(ts.values()))
assert diff < 2, f"Cameras not synced properly: frame_id={start+i}, {diff=:.1f}ms, {ts=}" assert diff < 2, f"Cameras not synced properly: frame_id={start+i}, {diff=:.1f}ms, {ts=}"
# driver camera should be staggered ~25ms from road camera
offset_ms = abs(self.ts[cams[2]]['timestampSof'][i] - self.ts[cams[0]]['timestampSof'][i]) / 1e6
assert 20 < offset_ms < 30, f"driver camera stagger out of range at frame {start+i}: {offset_ms:.1f}ms"
def test_camera_encoder_matches(self, subtests): def test_camera_encoder_matches(self, subtests):
# sanity check that the frame metadata is consistent with the encoded frames # sanity check that the frame metadata is consistent with the encoded frames
pairs = [('roadCameraState', 'roadEncodeIdx'), pairs = [('roadCameraState', 'roadEncodeIdx'),
-3
View File
@@ -1,4 +1 @@
installer/installers/* installer/installers/*
tests/diff/report
.coverage
+32 -30
View File
@@ -1,3 +1,4 @@
import re
from pathlib import Path from pathlib import Path
Import('env', 'arch', 'common') Import('env', 'arch', 'common')
@@ -18,38 +19,39 @@ env.Command(
if GetOption('extras') and arch == "larch64": if GetOption('extras') and arch == "larch64":
# build installers # build installers
raylib_env = env.Clone() if arch != "Darwin":
raylib_env['LIBPATH'] += [f'#third_party/raylib/{arch}/'] raylib_env = env.Clone()
raylib_env['LINKFLAGS'].append('-Wl,-strip-debug') raylib_env['LIBPATH'] += [f'#third_party/raylib/{arch}/']
raylib_env['LINKFLAGS'].append('-Wl,-strip-debug')
raylib_libs = common + ["raylib"] raylib_libs = common + ["raylib"]
if arch == "larch64": if arch == "larch64":
raylib_libs += ["GLESv2", "EGL", "gbm", "drm"] raylib_libs += ["GLESv2", "EGL", "gbm", "drm"]
else: else:
raylib_libs += ["GL"] raylib_libs += ["GL"]
release = "release3" release = "release3"
installers = [ installers = [
("openpilot", release), ("openpilot", release),
("openpilot_test", f"{release}-staging"), ("openpilot_test", f"{release}-staging"),
("openpilot_nightly", "nightly"), ("openpilot_nightly", "nightly"),
("openpilot_internal", "nightly-dev"), ("openpilot_internal", "nightly-dev"),
] ]
cont = raylib_env.Command("installer/continue_openpilot.o", "installer/continue_openpilot.sh", cont = raylib_env.Command("installer/continue_openpilot.o", "installer/continue_openpilot.sh",
"ld -r -b binary -o $TARGET $SOURCE")
inter = raylib_env.Command("installer/inter_ttf.o", "installer/inter-ascii.ttf",
"ld -r -b binary -o $TARGET $SOURCE")
inter_bold = raylib_env.Command("installer/inter_bold.o", "../assets/fonts/Inter-Bold.ttf",
"ld -r -b binary -o $TARGET $SOURCE") "ld -r -b binary -o $TARGET $SOURCE")
inter_light = raylib_env.Command("installer/inter_light.o", "../assets/fonts/Inter-Light.ttf", inter = raylib_env.Command("installer/inter_ttf.o", "installer/inter-ascii.ttf",
"ld -r -b binary -o $TARGET $SOURCE") "ld -r -b binary -o $TARGET $SOURCE")
for name, branch in installers: inter_bold = raylib_env.Command("installer/inter_bold.o", "../assets/fonts/Inter-Bold.ttf",
d = {'BRANCH': f"'\"{branch}\"'"} "ld -r -b binary -o $TARGET $SOURCE")
if "internal" in name: inter_light = raylib_env.Command("installer/inter_light.o", "../assets/fonts/Inter-Light.ttf",
d['INTERNAL'] = "1" "ld -r -b binary -o $TARGET $SOURCE")
for name, branch in installers:
d = {'BRANCH': f"'\"{branch}\"'"}
if "internal" in name:
d['INTERNAL'] = "1"
obj = raylib_env.Object(f"installer/installers/installer_{name}.o", ["installer/installer.cc"], CPPDEFINES=d) obj = raylib_env.Object(f"installer/installers/installer_{name}.o", ["installer/installer.cc"], CPPDEFINES=d)
f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter, inter_bold, inter_light], LIBS=raylib_libs) f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter, inter_bold, inter_light], LIBS=raylib_libs)
# keep installers small # keep installers small
assert f[0].get_size() < 2500*1e3, f[0].get_size() assert f[0].get_size() < 2500*1e3, f[0].get_size()
-1
View File
@@ -62,7 +62,6 @@ class HomeLayout(Widget):
self._setup_callbacks() self._setup_callbacks()
def show_event(self): def show_event(self):
super().show_event()
self._exp_mode_button.show_event() self._exp_mode_button.show_event()
self.last_refresh = time.monotonic() self.last_refresh = time.monotonic()
self._refresh() self._refresh()
+1 -1
View File
@@ -94,7 +94,7 @@ class TrainingGuide(Widget):
def _render(self, _): def _render(self, _):
# Safeguard against fast tapping # Safeguard against fast tapping
step = min(self._step, len(self._textures) - 1) step = min(self._step, len(self._textures) - 1)
rl.draw_texture_ex(self._textures[step], rl.Vector2(0, 0), 0.0, 1.0, rl.WHITE) rl.draw_texture(self._textures[step], 0, 0, rl.WHITE)
# progress bar # progress bar
if 0 < step < len(STEP_RECTS) - 1: if 0 < step < len(STEP_RECTS) - 1:
@@ -104,7 +104,6 @@ class DeveloperLayout(Widget):
self._scroller.render(rect) self._scroller.render(rect)
def show_event(self): def show_event(self):
super().show_event()
self._scroller.show_event() self._scroller.show_event()
self._update_toggles() self._update_toggles()
-1
View File
@@ -75,7 +75,6 @@ class DeviceLayout(Widget):
self._power_off_btn.action_item.right_button.set_visible(ui_state.is_offroad()) self._power_off_btn.action_item.right_button.set_visible(ui_state.is_offroad())
def show_event(self): def show_event(self):
super().show_event()
self._scroller.show_event() self._scroller.show_event()
def _render(self, rect): def _render(self, rect):
+1 -1
View File
@@ -69,6 +69,7 @@ class SoftwareLayout(Widget):
# Branch switcher # Branch switcher
self._branch_btn = button_item(lambda: tr("Target Branch"), lambda: tr("SELECT"), callback=self._on_select_branch) 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_btn.action_item.set_value(ui_state.params.get("UpdaterTargetBranch") or "")
self._branch_dialog: MultiOptionDialog | None = None self._branch_dialog: MultiOptionDialog | None = None
@@ -82,7 +83,6 @@ class SoftwareLayout(Widget):
], line_separator=True, spacing=0) ], line_separator=True, spacing=0)
def show_event(self): def show_event(self):
super().show_event()
self._scroller.show_event() self._scroller.show_event()
def _render(self, rect): def _render(self, rect):
-1
View File
@@ -152,7 +152,6 @@ class TogglesLayout(Widget):
ui_state.personality = personality ui_state.personality = personality
def show_event(self): def show_event(self):
super().show_event()
self._scroller.show_event() self._scroller.show_event()
self._update_toggles() self._update_toggles()
+4 -4
View File
@@ -165,14 +165,14 @@ class Sidebar(Widget, SidebarSP):
# Settings button # Settings button
settings_down = mouse_down and rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN) settings_down = mouse_down and rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN)
tint = Colors.BUTTON_PRESSED if settings_down else Colors.BUTTON_NORMAL tint = Colors.BUTTON_PRESSED if settings_down else Colors.BUTTON_NORMAL
rl.draw_texture_ex(self._settings_img, rl.Vector2(SETTINGS_BTN.x, SETTINGS_BTN.y), 0.0, 1.0, tint) rl.draw_texture(self._settings_img, int(SETTINGS_BTN.x), int(SETTINGS_BTN.y), tint)
# Home/Flag button # Home/Flag button
flag_pressed = mouse_down and rl.check_collision_point_rec(mouse_pos, HOME_BTN) flag_pressed = mouse_down and rl.check_collision_point_rec(mouse_pos, HOME_BTN)
button_img = self._flag_img if ui_state.started else self._home_img button_img = self._flag_img if ui_state.started else self._home_img
tint = Colors.BUTTON_PRESSED if (ui_state.started and flag_pressed) else Colors.BUTTON_NORMAL tint = Colors.BUTTON_PRESSED if (ui_state.started and flag_pressed) else Colors.BUTTON_NORMAL
rl.draw_texture_ex(button_img, rl.Vector2(HOME_BTN.x, HOME_BTN.y), 0.0, 1.0, tint) rl.draw_texture(button_img, int(HOME_BTN.x), int(HOME_BTN.y), tint)
# Microphone button # Microphone button
if self._recording_audio: if self._recording_audio:
@@ -182,8 +182,8 @@ class Sidebar(Widget, SidebarSP):
bg_color = rl.Color(Colors.DANGER.r, Colors.DANGER.g, Colors.DANGER.b, int(255 * 0.65)) if mic_pressed else Colors.DANGER bg_color = rl.Color(Colors.DANGER.r, Colors.DANGER.g, Colors.DANGER.b, int(255 * 0.65)) if mic_pressed else Colors.DANGER
rl.draw_rectangle_rounded(self._mic_indicator_rect, 1, 10, bg_color) rl.draw_rectangle_rounded(self._mic_indicator_rect, 1, 10, bg_color)
rl.draw_texture_ex(self._mic_img, rl.Vector2(self._mic_indicator_rect.x + (self._mic_indicator_rect.width - self._mic_img.width) / 2, rl.draw_texture(self._mic_img, int(self._mic_indicator_rect.x + (self._mic_indicator_rect.width - self._mic_img.width) / 2),
self._mic_indicator_rect.y + (self._mic_indicator_rect.height - self._mic_img.height) / 2), 0.0, 1.0, Colors.WHITE) int(self._mic_indicator_rect.y + (self._mic_indicator_rect.height - self._mic_img.height) / 2), Colors.WHITE)
def _draw_network_indicator(self, rect: rl.Rectangle): def _draw_network_indicator(self, rect: rl.Rectangle):
# Signal strength dots # Signal strength dots
+10 -11
View File
@@ -7,7 +7,7 @@ from collections.abc import Callable
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.layouts import HBoxLayout from openpilot.system.ui.widgets.layouts import HBoxLayout
from openpilot.system.ui.widgets.icon_widget import IconWidget from openpilot.system.ui.widgets.icon_widget import IconWidget
from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.label import MiciLabel, UnifiedLabel
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.version import RELEASE_BRANCHES from openpilot.system.version import RELEASE_BRANCHES
@@ -77,7 +77,7 @@ class NetworkIcon(Widget):
# Offset by difference in height between slashless and slash icons to make center align match # Offset by difference in height between slashless and slash icons to make center align match
draw_y -= (self._wifi_slash_txt.height - self._wifi_none_txt.height) / 2 draw_y -= (self._wifi_slash_txt.height - self._wifi_none_txt.height) / 2
rl.draw_texture_ex(draw_net_txt, rl.Vector2(draw_x, draw_y), 0.0, 1.0, rl.Color(255, 255, 255, int(255 * 0.9))) rl.draw_texture(draw_net_txt, int(draw_x), int(draw_y), rl.Color(255, 255, 255, int(255 * 0.9)))
class MiciHomeLayout(Widget): class MiciHomeLayout(Widget):
@@ -103,15 +103,14 @@ class MiciHomeLayout(Widget):
self._mic_icon, self._mic_icon,
], spacing=18) ], spacing=18)
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False) self._openpilot_label = MiciLabel("sunnypilot", font_size=90, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.AUDIOWIDE)
self._version_label = UnifiedLabel("", font_size=36, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False) self._version_label = MiciLabel("", font_size=36, font_weight=FontWeight.ROMAN)
self._large_version_label = UnifiedLabel("", font_size=64, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False) self._large_version_label = MiciLabel("", font_size=64, color=rl.GRAY, font_weight=FontWeight.ROMAN)
self._date_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False) self._date_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
self._branch_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, scroll=True) self._branch_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, scroll=True)
self._version_commit_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False) self._version_commit_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
def show_event(self): def show_event(self):
super().show_event()
self._version_text = self._get_version_text() self._version_text = self._get_version_text()
self._update_params() self._update_params()
@@ -183,12 +182,12 @@ class MiciHomeLayout(Widget):
self._version_label.render() self._version_label.render()
self._date_label.set_text(" " + self._version_text[3]) self._date_label.set_text(" " + self._version_text[3])
self._date_label.set_position(version_pos.x + self._version_label.text_width + 10, version_pos.y) self._date_label.set_position(version_pos.x + self._version_label.rect.width + 10, version_pos.y)
self._date_label.render() self._date_label.render()
self._branch_label.set_max_width(gui_app.width - self._version_label.text_width - self._date_label.text_width - 32) self._branch_label.set_max_width(gui_app.width - self._version_label.rect.width - self._date_label.rect.width - 32)
self._branch_label.set_text(" " + ("release" if release_branch else self._version_text[1])) self._branch_label.set_text(" " + ("release" if release_branch else self._version_text[1]))
self._branch_label.set_position(version_pos.x + self._version_label.text_width + self._date_label.text_width + 20, version_pos.y) self._branch_label.set_position(version_pos.x + self._version_label.rect.width + self._date_label.rect.width + 20, version_pos.y)
self._branch_label.render() self._branch_label.render()
if not release_branch: if not release_branch:
+3 -3
View File
@@ -56,7 +56,7 @@ class MiciMainLayout(Scroller):
gui_app.push_widget(self) gui_app.push_widget(self)
# Start onboarding if terms or training not completed, make sure to push after self # Start onboarding if terms or training not completed, make sure to push after self
self._onboarding_window = OnboardingWindow(lambda: gui_app.pop_widgets_to(self)) self._onboarding_window = OnboardingWindow()
if not self._onboarding_window.completed: if not self._onboarding_window.completed:
gui_app.push_widget(self._onboarding_window) gui_app.push_widget(self._onboarding_window)
@@ -82,7 +82,7 @@ class MiciMainLayout(Scroller):
def _handle_transitions(self): def _handle_transitions(self):
# Don't pop if onboarding # Don't pop if onboarding
if gui_app.widget_in_stack(self._onboarding_window): if gui_app.get_active_widget() == self._onboarding_window:
return return
if ui_state.started != self._prev_onroad: if ui_state.started != self._prev_onroad:
@@ -108,7 +108,7 @@ class MiciMainLayout(Scroller):
def _on_interactive_timeout(self): def _on_interactive_timeout(self):
# Don't pop if onboarding # Don't pop if onboarding
if gui_app.widget_in_stack(self._onboarding_window): if gui_app.get_active_widget() == self._onboarding_window:
return return
if ui_state.started: if ui_state.started:
+2 -2
View File
@@ -144,7 +144,7 @@ class AlertItem(Widget):
bg_texture = self._bg_small_pressed if self.is_pressed else self._bg_small bg_texture = self._bg_small_pressed if self.is_pressed else self._bg_small
# Draw background # Draw background
rl.draw_texture_ex(bg_texture, rl.Vector2(self._rect.x, self._rect.y), 0.0, 1.0, rl.WHITE) rl.draw_texture(bg_texture, int(self._rect.x), int(self._rect.y), rl.WHITE)
# Calculate text area (left side, avoiding icon on right) # Calculate text area (left side, avoiding icon on right)
title_width = self.ALERT_WIDTH - (self.ALERT_PADDING * 2) - self.ICON_SIZE - self.ICON_MARGIN title_width = self.ALERT_WIDTH - (self.ALERT_PADDING * 2) - self.ICON_SIZE - self.ICON_MARGIN
@@ -183,7 +183,7 @@ class AlertItem(Widget):
icon_texture = self._icon_orange icon_texture = self._icon_orange
icon_x = self._rect.x + self.ALERT_WIDTH - self.ALERT_PADDING - self.ICON_SIZE icon_x = self._rect.x + self.ALERT_WIDTH - self.ALERT_PADDING - self.ICON_SIZE
icon_y = self._rect.y + self.ALERT_PADDING icon_y = self._rect.y + self.ALERT_PADDING
rl.draw_texture_ex(icon_texture, rl.Vector2(icon_x, icon_y), 0.0, 1.0, rl.WHITE) rl.draw_texture(icon_texture, int(icon_x), int(icon_y), rl.WHITE)
class MiciOffroadAlerts(Scroller): class MiciOffroadAlerts(Scroller):
+318 -232
View File
@@ -1,24 +1,32 @@
from enum import IntEnum
import weakref
import math import math
import numpy as np import numpy as np
import qrcode
import pyray as rl import pyray as rl
from collections.abc import Callable
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import SmallCircleIconButton from openpilot.system.ui.widgets.button import SmallButton, SmallCircleIconButton
from openpilot.system.ui.widgets.scroller import NavScroller, Scroller from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.widgets.slider import SmallSlider
from openpilot.system.ui.mici_setup import GreyBigButton, BigPillButton 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.label import gui_label from openpilot.system.ui.widgets.label import gui_label
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.version import terms_version, training_version, terms_version_sp from openpilot.system.version import terms_version, training_version, terms_version_sp
from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined
from openpilot.selfdrive.ui.ui_state import ui_state, device from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkOnboarding
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationCircleButton
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import BaseDriverCameraDialog class OnboardingState(IntEnum):
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkConsentPage TERMS = 0
ONBOARDING = 1
DECLINE = 2
SUNNYLINK_CONSENT = 3
class DriverCameraSetupDialog(BaseDriverCameraDialog): class DriverCameraSetupDialog(BaseDriverCameraDialog):
@@ -52,62 +60,91 @@ class DriverCameraSetupDialog(BaseDriverCameraDialog):
rl.end_scissor_mode() rl.end_scissor_mode()
class TrainingGuidePreDMTutorial(NavScroller): class TrainingGuidePreDMTutorial(SetupTermsPage):
def __init__(self, continue_callback: Callable[[], None]): def __init__(self, continue_callback):
super().__init__() 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))
continue_button = BigPillButton("next") self._dm_label = UnifiedLabel("Next, we'll ensure comma four is mounted properly.\n\nIf it does not have a clear view of the driver, " +
continue_button.set_click_callback(continue_callback) "unplug and remount before continuing.", 42,
FontWeight.ROMAN)
self._scroller.add_widgets([
GreyBigButton("driver monitoring\ncheck", "scroll to continue",
gui_app.texture("icons_mici/setup/green_dm.png", 64, 64)),
GreyBigButton("", "Next, we'll check if comma four can detect the driver properly."),
GreyBigButton("", "sunnypilot uses the cabin camera to check if the driver is distracted."),
GreyBigButton("", "If it does not have a clear view of the driver, unplug and remount before continuing."),
continue_button,
])
def show_event(self): def show_event(self):
super().show_event() super().show_event()
# Get driver monitoring model ready for next step # Get driver monitoring model ready for next step
ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", True) 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)),
))
class DMBadFaceDetected(NavScroller): class DMBadFaceDetected(SetupTermsPage):
def __init__(self): def __init__(self, continue_callback, back_callback):
super().__init__() 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)
back_button = BigPillButton("back") @property
back_button.set_click_callback(self.dismiss) def _content_height(self):
return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset()
self._scroller.add_widgets([ def _render_content(self, scroll_offset):
GreyBigButton("looking for driver", "make sure comma\nfour can see your face", self._title_header.render(rl.Rectangle(
gui_app.texture("icons_mici/setup/orange_dm.png", 64, 64)), self._rect.x + 16,
GreyBigButton("", "Remount if your face is blocked, or driver monitoring has difficulty tracking your face."), self._rect.y + 16 + scroll_offset,
back_button, 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 TrainingGuideDMTutorial(NavWidget): class TrainingGuideDMTutorial(Widget):
PROGRESS_DURATION = 4 PROGRESS_DURATION = 4
LOOKING_THRESHOLD_DEG = 30.0 LOOKING_THRESHOLD_DEG = 30.0
def __init__(self, continue_callback: Callable[[], None]): def __init__(self, continue_callback):
super().__init__() super().__init__()
self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 28, 48)) self_ref = weakref.ref(self)
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
self._good_button.set_click_callback(continue_callback) 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._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 42, 42))
# 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_enabled(False) self._good_button.set_enabled(False)
self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps) self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps)
self._dialog = DriverCameraSetupDialog() self._dialog = DriverCameraSetupDialog()
self._bad_face_page = DMBadFaceDetected() self._bad_face_page = DMBadFaceDetected(HARDWARE.shutdown, lambda: self_ref() and self_ref()._hide_bad_face_page())
self._should_show_bad_face_page = False
# Disable driver monitoring model when device times out for inactivity # Disable driver monitoring model when device times out for inactivity
def inactivity_callback(): def inactivity_callback():
@@ -115,11 +152,23 @@ class TrainingGuideDMTutorial(NavWidget):
device.add_interactive_timeout_callback(inactivity_callback) device.add_interactive_timeout_callback(inactivity_callback)
def _show_bad_face_page(self):
self._bad_face_page.show_event()
self.hide_event()
self._should_show_bad_face_page = True
def _hide_bad_face_page(self):
self._bad_face_page.hide_event()
self.show_event()
self._should_show_bad_face_page = False
def show_event(self): def show_event(self):
super().show_event() super().show_event()
self._dialog.show_event() self._dialog.show_event()
self._progress.x = 0.0 self._progress.x = 0.0
device.set_offroad_brightness(100)
def _update_state(self): def _update_state(self):
super()._update_state() super()._update_state()
if device.awake and not ui_state.params.get_bool("IsDriverViewEnabled"): if device.awake and not ui_state.params.get_bool("IsDriverViewEnabled"):
@@ -139,8 +188,7 @@ class TrainingGuideDMTutorial(NavWidget):
looking_center = False looking_center = False
# stay at 100% once reached # stay at 100% once reached
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:
if ((dm_state.faceDetected and looking_center) or self._progress.x > 0.99) and not in_bad_face:
slow = self._progress.x < 0.25 slow = self._progress.x < 0.25
duration = self.PROGRESS_DURATION * 2 if slow else self.PROGRESS_DURATION duration = self.PROGRESS_DURATION * 2 if slow else self.PROGRESS_DURATION
self._progress.x += 1.0 / (duration * gui_app.target_fps) self._progress.x += 1.0 / (duration * gui_app.target_fps)
@@ -151,12 +199,13 @@ class TrainingGuideDMTutorial(NavWidget):
self._good_button.set_enabled(self._progress.x >= 0.999) self._good_button.set_enabled(self._progress.x >= 0.999)
def _render(self, _): def _render(self, _):
if self._should_show_bad_face_page:
return self._bad_face_page.render(self._rect)
self._dialog.render(self._rect) self._dialog.render(self._rect)
gradient_y = int(self._rect.y + self._rect.height - 80) rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - 80),
gradient_h = int(self._rect.y) + int(self._rect.height) - gradient_y int(self._rect.width), 80, rl.BLANK, rl.BLACK)
rl.draw_rectangle_gradient_v(int(self._rect.x), gradient_y,
int(self._rect.width), gradient_h, rl.BLANK, rl.BLACK)
# draw white ring around dm icon to indicate progress # draw white ring around dm icon to indicate progress
ring_thickness = 8 ring_thickness = 8
@@ -209,229 +258,266 @@ class TrainingGuideDMTutorial(NavWidget):
)) ))
# rounded border # rounded border
rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height))
rl.draw_rectangle_rounded_lines_ex(self._rect, 0.2 * 1.02, 10, 50, rl.BLACK) rl.draw_rectangle_rounded_lines_ex(self._rect, 0.2 * 1.02, 10, 50, rl.BLACK)
rl.end_scissor_mode()
class TrainingGuideRecordFront(NavScroller): class TrainingGuideRecordFront(SetupTermsPage):
def __init__(self, continue_callback: Callable[[], None]): def __init__(self, continue_callback):
super().__init__() def on_back():
ui_state.params.put_bool("RecordFront", False)
def on_accept():
ui_state.params.put_bool_nonblocking("RecordFront", True)
continue_callback() continue_callback()
def on_decline(): def on_continue():
ui_state.params.put_bool_nonblocking("RecordFront", False) ui_state.params.put_bool("RecordFront", True)
continue_callback() continue_callback()
self._accept_button = BigConfirmationCircleButton("allow data uploading", gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 64, 64), super().__init__(on_continue, back_callback=on_back, back_text="no", continue_text="yes")
on_accept, exit_on_confirm=False) self._title_header = TermsHeader("improve driver monitoring", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
self._decline_button = BigConfirmationCircleButton("no, don't upload", gui_app.texture("icons_mici/setup/cancel.png", 64, 64), on_decline, self._dm_label = UnifiedLabel("Do you want to upload driver camera data?", 42,
exit_on_confirm=False) FontWeight.ROMAN)
self._scroller.add_widgets([ def show_event(self):
GreyBigButton("driver camera data", "do you want to share video data for training?", super().show_event()
gui_app.texture("icons_mici/setup/green_dm.png", 64, 64)), # Disable driver monitoring model after last step
GreyBigButton("", "Sharing your data with comma helps improve openpilot and sunnypilot for everyone."), ui_state.params.put_bool("IsDriverViewEnabled", False)
self._accept_button,
self._decline_button, @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(Scroller): class TrainingGuideAttentionNotice(SetupTermsPage):
def __init__(self, continue_callback: Callable[[], None]): def __init__(self, continue_callback):
super().__init__() 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)
continue_button = BigPillButton("next") @property
continue_button.set_click_callback(continue_callback) def _content_height(self):
return self._warning_label.rect.y + self._warning_label.rect.height - self._scroll_panel.get_offset()
self._scroller.add_widgets([ def _render_content(self, scroll_offset):
GreyBigButton("what is sunnypilot?", "scroll to continue", self._title_header.render(rl.Rectangle(
gui_app.texture("icons_mici/setup/green_info.png", 64, 64)), self._rect.x + 16,
GreyBigButton("", "1. sunnypilot is a driver assistance system."), self._rect.y + 16 + scroll_offset,
GreyBigButton("", "2. You must pay attention at all times."), self._title_header.rect.width,
GreyBigButton("", "3. You must be ready to take over at any time."), self._title_header.rect.height,
GreyBigButton("", "4. You are fully responsible for driving the car."), ))
continue_button,
]) 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(NavWidget): class TrainingGuide(Widget):
def __init__(self, completed_callback: Callable[[], None]): def __init__(self, completed_callback=None):
super().__init__()
self._steps = [
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._child(self._steps[0])
self._steps[0].set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack
def _render(self, _):
self._steps[0].render(self._rect)
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(round(self._rect.x), round(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__()
self._accept_button = BigConfirmationCircleButton("accept\nterms", gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 64, 64), on_accept)
self._decline_button = BigConfirmationCircleButton("decline &\nuninstall", gui_app.texture("icons_mici/setup/cancel.png", 64, 64), on_decline,
red=True, exit_on_confirm=False)
self._terms_header = GreyBigButton("terms of\nservice", "scroll to continue",
gui_app.texture("icons_mici/setup/green_info.png", 64, 64))
self._must_accept_card = GreyBigButton("", "You must accept the Terms of Service to use sunnypilot.")
self._scroller.add_widgets([
self._terms_header,
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"),
self._must_accept_card,
self._accept_button,
self._decline_button,
])
def _render(self, _):
rl.draw_rectangle_rec(self._rect, rl.BLACK)
super()._render(_)
class OnboardingWindow(Widget):
def __init__(self, completed_callback: Callable[[], None]):
super().__init__() super().__init__()
self._completed_callback = completed_callback self._completed_callback = completed_callback
self._accepted_terms: bool = (ui_state.params.get("HasAcceptedTerms") == terms_version and self._step = 0
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.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) self_ref = weakref.ref(self)
# Windows — all pushed onto nav stack, _terms is always rendered as base layer def on_continue():
self._terms = TermsPage(on_accept=self._on_terms_accepted, on_decline=self._on_uninstall) if obj := self_ref():
self._terms.set_enabled(lambda: self.enabled) # for nav stack obj._advance_step()
self._sunnylink_consent = SunnylinkConsentPage( self._steps = [
on_accept=self._on_sunnylink_accepted, TrainingGuideAttentionNotice(continue_callback=on_continue),
on_decline=self._on_sunnylink_declined, TrainingGuidePreDMTutorial(continue_callback=on_continue),
) TrainingGuideDMTutorial(continue_callback=on_continue),
TrainingGuideRecordFront(continue_callback=on_continue),
self._training_guide = TrainingGuide(completed_callback=self._on_completed_training) ]
self._training_guide.set_enabled(lambda: self.enabled) # for nav stack
def show_event(self):
self._needs_initial_push = False super().show_event()
device.set_override_interactive_timeout(300)
def _on_uninstall(self):
ui_state.params.put_bool("DoUninstall", True) def hide_event(self):
super().hide_event()
device.set_override_interactive_timeout(None)
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()
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)),
))
class OnboardingWindow(Widget):
def __init__(self):
super().__init__()
self._accepted_terms: bool = ui_state.params.get("HasAcceptedTerms") == terms_version
self._training_done: bool = ui_state.params.get("CompletedTrainingVersion") == training_version
self._state = OnboardingState.TERMS if not self._accepted_terms else OnboardingState.ONBOARDING
self.set_rect(rl.Rectangle(0, 0, 458, gui_app.height))
# 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)
# 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
def show_event(self): def show_event(self):
super().show_event() super().show_event()
device.set_override_interactive_timeout(300) device.set_override_interactive_timeout(300)
device.set_offroad_brightness(100)
self._needs_initial_push = True
def hide_event(self): def hide_event(self):
super().hide_event() super().hide_event()
# FIXME: when nav stack sends hide event to widget 2 below on push, this needs to be moved
device.set_override_interactive_timeout(None) device.set_override_interactive_timeout(None)
device.set_offroad_brightness(None)
@property @property
def completed(self) -> bool: def completed(self) -> bool:
return self._accepted_terms and self._sunnylink_consent_done and self._training_done 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
def close(self): def close(self):
ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", False) ui_state.params.put_bool("IsDriverViewEnabled", False)
self._completed_callback() gui_app.pop_widget()
def _on_terms_accepted(self): def _on_terms_accepted(self):
ui_state.params.put("HasAcceptedTerms", terms_version) ui_state.params.put("HasAcceptedTerms", terms_version)
ui_state.params.put("HasAcceptedTermsSP", terms_version_sp) ui_state.params.put("HasAcceptedTermsSP", terms_version_sp)
self._accepted_terms = True if not self._sunnylink.completed:
if not self._sunnylink_consent_done: self._state = OnboardingState.SUNNYLINK_CONSENT
gui_app.push_widget(self._sunnylink_consent)
elif not self._training_done: elif not self._training_done:
gui_app.push_widget(self._training_guide) self._state = OnboardingState.ONBOARDING
else:
self.close()
def _on_sunnylink_accepted(self):
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
ui_state.params.put_bool("SunnylinkEnabled", True)
self._sunnylink_consent_done = True
if not self._training_done:
gui_app.push_widget(self._training_guide)
else:
self.close()
def _on_sunnylink_declined(self):
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_declined)
ui_state.params.put_bool("SunnylinkEnabled", False)
self._sunnylink_consent_done = True
if not self._training_done:
gui_app.push_widget(self._training_guide)
else: else:
self.close() self.close()
def _on_completed_training(self): def _on_completed_training(self):
ui_state.params.put("CompletedTrainingVersion", training_version) ui_state.params.put("CompletedTrainingVersion", training_version)
self._training_done = True
self.close() self.close()
def _render(self, _): def _render(self, _):
rl.draw_rectangle_rec(self._rect, rl.BLACK) rl.draw_rectangle_rec(self._rect, rl.BLACK)
if self._state == OnboardingState.TERMS:
# Deferred from show_event to avoid nested push_widget re-enable bug self._terms.render(self._rect)
if self._needs_initial_push: elif self._state == OnboardingState.SUNNYLINK_CONSENT:
self._needs_initial_push = False self._sunnylink.render(self._rect)
if self._accepted_terms and not self._sunnylink_consent_done: if self._sunnylink.completed:
gui_app.push_widget(self._sunnylink_consent) if not self._training_done:
elif self._accepted_terms and self._sunnylink_consent_done and not self._training_done: self._state = OnboardingState.ONBOARDING
gui_app.push_widget(self._training_guide) else:
self.close()
self._terms.render(self._rect) 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)
+13 -22
View File
@@ -5,37 +5,32 @@ from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigInputDialog
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.widgets.ssh_key import SshKeyFetcher from openpilot.selfdrive.ui.widgets.ssh_key import SshKeyAction
class DeveloperLayoutMici(NavScroller): class DeveloperLayoutMici(NavScroller):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._ssh_fetcher = SshKeyFetcher(ui_state.params)
def github_username_callback(username: str): def github_username_callback(username: str):
if username: if username:
self._ssh_keys_btn.set_value("Loading...") ssh_keys = SshKeyAction()
self._ssh_keys_btn.set_enabled(False) ssh_keys._fetch_ssh_key(username)
if not ssh_keys._error_message:
def on_response(error): self._ssh_keys_btn.set_value(username)
self._ssh_keys_btn.set_enabled(True) else:
if error is None: dlg = BigDialog("", ssh_keys._error_message)
self._ssh_keys_btn.set_value(username) gui_app.push_widget(dlg)
else:
self._ssh_keys_btn.set_value("Not set")
gui_app.push_widget(BigDialog("", error))
self._ssh_fetcher.fetch(username, on_response)
else: else:
self._ssh_fetcher.clear() ui_state.params.remove("GithubUsername")
ui_state.params.remove("GithubSshKeys")
self._ssh_keys_btn.set_value("Not set") self._ssh_keys_btn.set_value("Not set")
def ssh_keys_callback(): def ssh_keys_callback():
github_username = ui_state.params.get("GithubUsername") or "" github_username = ui_state.params.get("GithubUsername") or ""
dlg = BigInputDialog("enter GitHub username...", github_username, minimum_length=0, confirm_callback=github_username_callback) dlg = BigInputDialog("enter GitHub username...", github_username, minimum_length=0, confirm_callback=github_username_callback)
if not system_time_valid(): if not system_time_valid():
dlg = BigDialog("", "Please connect to Wi-Fi to fetch your key.") dlg = BigDialog("Please connect to Wi-Fi to fetch your key", "")
gui_app.push_widget(dlg) gui_app.push_widget(dlg)
return return
gui_app.push_widget(dlg) gui_app.push_widget(dlg)
@@ -47,8 +42,8 @@ class DeveloperLayoutMici(NavScroller):
# adb, ssh, ssh keys, debug mode, joystick debug mode, longitudinal maneuver mode, ip address # adb, ssh, ssh keys, debug mode, joystick debug mode, longitudinal maneuver mode, ip address
# ******** Main Scroller ******** # ******** Main Scroller ********
self._adb_toggle = BigCircleParamControl(gui_app.texture("icons_mici/adb_short.png", 82, 82), "AdbEnabled", icon_offset=(0, 12)) self._adb_toggle = BigCircleParamControl("icons_mici/adb_short.png", "AdbEnabled", icon_size=(82, 82), icon_offset=(0, 12))
self._ssh_toggle = BigCircleParamControl(gui_app.texture("icons_mici/ssh_short.png", 82, 82), "SshEnabled", icon_offset=(0, 12)) self._ssh_toggle = BigCircleParamControl("icons_mici/ssh_short.png", "SshEnabled", icon_size=(82, 82), icon_offset=(0, 12))
self._joystick_toggle = BigToggle("joystick debug mode", self._joystick_toggle = BigToggle("joystick debug mode",
initial_state=ui_state.params.get_bool("JoystickDebugMode"), initial_state=ui_state.params.get_bool("JoystickDebugMode"),
toggle_callback=self._on_joystick_debug_mode) toggle_callback=self._on_joystick_debug_mode)
@@ -104,10 +99,6 @@ class DeveloperLayoutMici(NavScroller):
ui_state.add_offroad_transition_callback(self._update_toggles) ui_state.add_offroad_transition_callback(self._update_toggles)
def _update_state(self):
super()._update_state()
self._ssh_fetcher.update()
def show_event(self): def show_event(self):
super().show_event() super().show_event()
self._update_toggles() self._update_toggles()
+48 -67
View File
@@ -9,40 +9,19 @@ from openpilot.common.params import Params
from openpilot.common.time_helpers import system_time_valid from openpilot.common.time_helpers import system_time_valid
from openpilot.system.ui.widgets.scroller import NavRawScrollPanel, NavScroller from openpilot.system.ui.widgets.scroller import NavRawScrollPanel, NavScroller
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.ui_state import device, ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.label import MiciLabel
from openpilot.system.ui.widgets.html_render import HtmlModal, HtmlRenderer from openpilot.system.ui.widgets.html_render import HtmlModal, HtmlRenderer
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
class ReviewTermsPage(TermsPage, NavScroller):
"""TermsPage with NavWidget swipe-to-dismiss for reviewing in device settings."""
def __init__(self):
super().__init__(on_accept=self.dismiss, on_decline=self.dismiss)
self._terms_header.set_visible(False)
self._must_accept_card.set_visible(False)
self._accept_button.set_visible(False)
self._decline_button.set_visible(False)
class ReviewTrainingGuide(TrainingGuide):
def show_event(self):
super().show_event()
device.set_override_interactive_timeout(300)
def hide_event(self):
super().hide_event()
device.set_override_interactive_timeout(None)
ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", False)
class MiciFccModal(NavRawScrollPanel): class MiciFccModal(NavRawScrollPanel):
def __init__(self, file_path: str | None = None, text: str | None = None): def __init__(self, file_path: str | None = None, text: str | None = None):
super().__init__() super().__init__()
@@ -64,31 +43,34 @@ class MiciFccModal(NavRawScrollPanel):
rl.draw_texture_ex(self._fcc_logo, fcc_pos, 0.0, 1.0, rl.WHITE) rl.draw_texture_ex(self._fcc_logo, fcc_pos, 0.0, 1.0, rl.WHITE)
def _engaged_confirmation_click(callback: Callable, action_text: str, icon: rl.Texture, exit_on_confirm: bool = True, red: bool = False): def _engaged_confirmation_callback(callback: Callable, action_text: str):
if not ui_state.engaged: if not ui_state.engaged:
def confirm_callback(): def confirm_callback():
# Check engaged again in case it changed while the dialog was open # Check engaged again in case it changed while the dialog was open
# TODO: if true, we stay on the dialog if not exit_on_confirm until normal onroad timeout
if not ui_state.engaged: if not ui_state.engaged:
callback() callback()
gui_app.push_widget(BigConfirmationDialog(f"slide to\n{action_text.lower()}", icon, confirm_callback, exit_on_confirm=exit_on_confirm, red=red)) red = False
if action_text == "power off":
icon = "icons_mici/settings/device/power.png"
red = True
elif action_text == "reboot":
icon = "icons_mici/settings/device/reboot.png"
elif action_text == "reset":
icon = "icons_mici/settings/device/lkas.png"
elif action_text == "uninstall":
icon = "icons_mici/settings/device/uninstall.png"
else:
# TODO: check
icon = "icons_mici/settings/comma_icon.png"
dlg: BigConfirmationDialogV2 | BigDialog = BigConfirmationDialogV2(f"slide to\n{action_text.lower()}", icon, red=red,
exit_on_confirm=action_text == "reset",
confirm_callback=confirm_callback)
gui_app.push_widget(dlg)
else: else:
gui_app.push_widget(BigDialog("", f"Disengage to {action_text}")) dlg = BigDialog(f"Disengage to {action_text}", "")
gui_app.push_widget(dlg)
class EngagedConfirmationCircleButton(BigCircleButton):
def __init__(self, title: str, icon: rl.Texture, callback: Callable[[], None], exit_on_confirm: bool = True,
red: bool = False, icon_offset: tuple[int, int] = (0, 0)):
super().__init__(icon, red, icon_offset)
self.set_click_callback(lambda: _engaged_confirmation_click(callback, title, icon, exit_on_confirm=exit_on_confirm, red=red))
class EngagedConfirmationButton(BigButton):
def __init__(self, text: str, action_text: str, icon: rl.Texture, callback: Callable[[], None],
exit_on_confirm: bool = True, red: bool = False):
super().__init__(text, "", icon)
self.set_click_callback(lambda: _engaged_confirmation_click(callback, action_text, icon, exit_on_confirm=exit_on_confirm, red=red))
class DeviceInfoLayoutMici(Widget): class DeviceInfoLayoutMici(Widget):
@@ -98,15 +80,14 @@ class DeviceInfoLayoutMici(Widget):
self.set_rect(rl.Rectangle(0, 0, 360, 180)) self.set_rect(rl.Rectangle(0, 0, 360, 180))
params = Params() params = Params()
header_color = rl.Color(255, 255, 255, int(255 * 0.9))
subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))
max_width = int(self._rect.width - 20) max_width = int(self._rect.width - 20)
self._dongle_id_label = UnifiedLabel("device ID", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) self._dongle_id_label = MiciLabel("device ID", 48, width=max_width, color=header_color, font_weight=FontWeight.DISPLAY)
self._dongle_id_text_label = UnifiedLabel(params.get("DongleId") or 'N/A', 32, max_width=max_width, text_color=subheader_color, self._dongle_id_text_label = MiciLabel(params.get("DongleId") or 'N/A', 32, width=max_width, color=subheader_color, font_weight=FontWeight.ROMAN)
font_weight=FontWeight.ROMAN, wrap_text=False)
self._serial_number_label = UnifiedLabel("serial", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) self._serial_number_label = MiciLabel("serial", 48, color=header_color, font_weight=FontWeight.DISPLAY)
self._serial_number_text_label = UnifiedLabel(params.get("HardwareSerial") or 'N/A', 32, max_width=max_width, text_color=subheader_color, self._serial_number_text_label = MiciLabel(params.get("HardwareSerial") or 'N/A', 32, width=max_width, color=subheader_color, font_weight=FontWeight.ROMAN)
font_weight=FontWeight.ROMAN, wrap_text=False)
def _render(self, _): def _render(self, _):
self._dongle_id_label.set_position(self._rect.x + 20, self._rect.y - 10) self._dongle_id_label.set_position(self._rect.x + 20, self._rect.y - 10)
@@ -130,7 +111,7 @@ class UpdaterState(IntEnum):
class PairBigButton(BigButton): class PairBigButton(BigButton):
def __init__(self): def __init__(self):
super().__init__("pair", "connect.comma.ai", gui_app.texture("icons_mici/settings/comma_icon.png", 33, 60)) super().__init__("pair", "connect.comma.ai", "icons_mici/settings/comma_icon.png", icon_size=(33, 60))
def _get_label_font_size(self): def _get_label_font_size(self):
return 64 return 64
@@ -156,9 +137,9 @@ class PairBigButton(BigButton):
return return
dlg: BigDialog | PairingDialog dlg: BigDialog | PairingDialog
if not system_time_valid(): if not system_time_valid():
dlg = BigDialog("", tr("Please connect to Wi-Fi to complete initial pairing.")) dlg = BigDialog(tr("Please connect to Wi-Fi to complete initial pairing"), "")
elif UNREGISTERED_DONGLE_ID == (ui_state.params.get("DongleId") or UNREGISTERED_DONGLE_ID): elif UNREGISTERED_DONGLE_ID == (ui_state.params.get("DongleId") or UNREGISTERED_DONGLE_ID):
dlg = BigDialog("", tr("Device must be registered with the comma.ai backend to pair.")) dlg = BigDialog(tr("Device must be registered with the comma.ai backend to pair"), "")
else: else:
dlg = PairingDialog() dlg = PairingDialog()
gui_app.push_widget(dlg) gui_app.push_widget(dlg)
@@ -188,7 +169,7 @@ class UpdateOpenpilotBigButton(BigButton):
super()._handle_mouse_release(mouse_pos) super()._handle_mouse_release(mouse_pos)
if not system_time_valid(): if not system_time_valid():
dlg = BigDialog("", tr("Please connect to Wi-Fi to update.")) dlg = BigDialog(tr("Please connect to Wi-Fi to update"), "")
gui_app.push_widget(dlg) gui_app.push_widget(dlg)
return return
@@ -309,33 +290,33 @@ class DeviceLayoutMici(NavScroller):
def uninstall_openpilot_callback(): def uninstall_openpilot_callback():
ui_state.params.put_bool("DoUninstall", True) ui_state.params.put_bool("DoUninstall", True)
reset_calibration_btn = EngagedConfirmationButton("reset calibration", "reset", gui_app.texture("icons_mici/settings/device/lkas.png", 122, 64), reset_calibration_btn = BigButton("reset calibration", "", "icons_mici/settings/device/lkas.png", icon_size=(114, 60))
reset_calibration_callback) reset_calibration_btn.set_click_callback(lambda: _engaged_confirmation_callback(reset_calibration_callback, "reset"))
uninstall_openpilot_btn = EngagedConfirmationButton("uninstall sunnypilot", "uninstall", uninstall_openpilot_btn = BigButton("uninstall sunnypilot", "", "icons_mici/settings/device/uninstall.png")
gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64), uninstall_openpilot_btn.set_click_callback(lambda: _engaged_confirmation_callback(uninstall_openpilot_callback, "uninstall"))
uninstall_openpilot_callback, exit_on_confirm=False)
reboot_btn = EngagedConfirmationCircleButton("reboot", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70), reboot_btn = BigCircleButton("icons_mici/settings/device/reboot.png", red=False, icon_size=(64, 70))
reboot_callback, exit_on_confirm=False) reboot_btn.set_click_callback(lambda: _engaged_confirmation_callback(reboot_callback, "reboot"))
self._power_off_btn = EngagedConfirmationCircleButton("power off", gui_app.texture("icons_mici/settings/device/power.png", 64, 66), self._power_off_btn = BigCircleButton("icons_mici/settings/device/power.png", red=True, icon_size=(64, 66))
power_off_callback, exit_on_confirm=False, red=True) self._power_off_btn.set_click_callback(lambda: _engaged_confirmation_callback(power_off_callback, "power off"))
self._power_off_btn.set_visible(lambda: not ui_state.ignition) self._power_off_btn.set_visible(lambda: not ui_state.ignition)
regulatory_btn = BigButton("regulatory info", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64)) regulatory_btn = BigButton("regulatory info", "", "icons_mici/settings/device/info.png")
regulatory_btn.set_click_callback(self._on_regulatory) regulatory_btn.set_click_callback(self._on_regulatory)
driver_cam_btn = BigButton("driver\ncamera preview", "", gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64)) driver_cam_btn = BigButton("driver\ncamera preview", "", "icons_mici/settings/device/cameras.png")
driver_cam_btn.set_click_callback(lambda: gui_app.push_widget(DriverCameraDialog())) driver_cam_btn.set_click_callback(lambda: gui_app.push_widget(DriverCameraDialog()))
driver_cam_btn.set_enabled(lambda: ui_state.is_offroad()) driver_cam_btn.set_enabled(lambda: ui_state.is_offroad())
review_training_guide_btn = BigButton("review\ntraining guide", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64)) 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(ReviewTrainingGuide(completed_callback=lambda: gui_app.pop_widgets_to(self)))) review_training_guide_btn.set_click_callback(lambda: gui_app.push_widget(TrainingGuide(completed_callback=gui_app.pop_widget)))
review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad()) review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad())
terms_btn = BigButton("terms &\nconditions", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64)) terms_btn = BigButton("terms &\nconditions", "", "icons_mici/settings/device/info.png")
terms_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTermsPage())) terms_btn.set_click_callback(lambda: gui_app.push_widget(TermsPage(on_accept=gui_app.pop_widget)))
terms_btn.set_enabled(lambda: ui_state.is_offroad())
self._scroller.add_widgets([ self._scroller.add_widgets([
DeviceInfoLayoutMici(), DeviceInfoLayoutMici(),
@@ -81,12 +81,12 @@ class FirehoseLayoutBase(Widget):
def _render(self, rect: rl.Rectangle): def _render(self, rect: rl.Rectangle):
# compute total content height for scrolling # compute total content height for scrolling
content_height = self._measure_content_height(rect) content_height = self._measure_content_height(rect)
scroll_offset = self._scroll_panel.update(rect, content_height) scroll_offset = round(self._scroll_panel.update(rect, content_height))
# start drawing with offset # start drawing with offset
x = rect.x + 40 x = int(rect.x + 40)
y = rect.y + 40 + scroll_offset y = int(rect.y + 40 + scroll_offset)
w = rect.width - 80 w = int(rect.width - 80)
# Title # Title
title_text = tr(TITLE) title_text = tr(TITLE)
@@ -100,7 +100,7 @@ class FirehoseLayoutBase(Widget):
y += 20 y += 20
# Separator # Separator
rl.draw_rectangle_rec(rl.Rectangle(x, y, w, 2), self.GRAY) rl.draw_rectangle(x, y, w, 2, self.GRAY)
y += 20 y += 20
# Status # Status
@@ -116,7 +116,7 @@ class FirehoseLayoutBase(Widget):
y += 20 y += 20
# Separator # Separator
rl.draw_rectangle_rec(rl.Rectangle(x, y, w, 2), self.GRAY) rl.draw_rectangle(x, y, w, 2, self.GRAY)
y += 20 y += 20
# Instructions intro # Instructions intro
@@ -1,9 +1,13 @@
import pyray as rl import pyray as rl
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiIcon from openpilot.system.ui.widgets.scroller import NavScroller
from openpilot.selfdrive.ui.mici.widgets.button import BigButton 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.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.wifi_manager import WifiManager, ConnectStatus, SecurityType, normalize_ssid from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType, ConnectStatus, SecurityType, normalize_ssid
class WifiNetworkButton(BigButton): class WifiNetworkButton(BigButton):
@@ -58,3 +62,148 @@ class WifiNetworkButton(BigButton):
lock_x = icon_x + self._txt_icon.width - self._lock_txt.width + 7 lock_x = icon_x + self._txt_icon.width - self._lock_txt.width + 7
lock_y = icon_y + self._txt_icon.height - self._lock_txt.height + 8 lock_y = icon_y + self._txt_icon.height - self._lock_txt.height + 8
rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, 1.0, rl.WHITE) rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, 1.0, rl.WHITE)
class NetworkLayoutMici(NavScroller):
def __init__(self):
super().__init__()
self._wifi_manager = WifiManager()
self._wifi_manager.set_active(False)
self._wifi_ui = WifiUIMici(self._wifi_manager)
self._wifi_manager.add_callbacks(
networks_updated=self._on_network_updated,
)
# ******** Tethering ********
def tethering_toggle_callback(checked: bool):
self._tethering_toggle_btn.set_enabled(False)
self._tethering_password_btn.set_enabled(False)
self._network_metered_btn.set_enabled(False)
self._wifi_manager.set_tethering_active(checked)
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
def tethering_password_callback(password: str):
if password:
self._tethering_toggle_btn.set_enabled(False)
self._tethering_password_btn.set_enabled(False)
self._wifi_manager.set_tethering_password(password)
def tethering_password_clicked():
tethering_password = self._wifi_manager.tethering_password
dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8,
confirm_callback=tethering_password_callback)
gui_app.push_widget(dlg)
txt_tethering = gui_app.texture("icons_mici/settings/network/tethering.png", 64, 54)
self._tethering_password_btn = BigButton("tethering password", "", txt_tethering)
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
# ******** Network Metered ********
def network_metered_callback(value: str):
self._network_metered_btn.set_enabled(False)
metered = {
'default': MeteredType.UNKNOWN,
'metered': MeteredType.YES,
'unmetered': MeteredType.NO
}.get(value, MeteredType.UNKNOWN)
self._wifi_manager.set_current_network_metered(metered)
# TODO: signal for current network metered type when changing networks, this is wrong until you press it once
# TODO: disable when not connected
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
self._network_metered_btn.set_enabled(False)
self._wifi_button = WifiNetworkButton(self._wifi_manager)
self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui))
# ******** Advanced settings ********
# ******** Roaming toggle ********
self._roaming_btn = BigParamControl("enable roaming", "GsmRoaming", toggle_callback=self._toggle_roaming)
# ******** APN settings ********
self._apn_btn = BigButton("apn settings", "edit")
self._apn_btn.set_click_callback(self._edit_apn)
# ******** Cellular metered toggle ********
self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered", toggle_callback=self._toggle_cellular_metered)
# Main scroller ----------------------------------
self._scroller.add_widgets([
self._wifi_button,
self._network_metered_btn,
self._tethering_toggle_btn,
self._tethering_password_btn,
# /* Advanced settings
self._roaming_btn,
self._apn_btn,
self._cellular_metered_btn,
# */
])
# Set initial config
roaming_enabled = ui_state.params.get_bool("GsmRoaming")
metered = ui_state.params.get_bool("GsmMetered")
self._wifi_manager.update_gsm_settings(roaming_enabled, ui_state.params.get("GsmApn") or "", metered)
def _update_state(self):
super()._update_state()
# If not using prime SIM, show GSM settings and enable IPv4 forwarding
show_cell_settings = ui_state.prime_state.get_type() in (PrimeType.NONE, PrimeType.LITE)
self._wifi_manager.set_ipv4_forward(show_cell_settings)
self._roaming_btn.set_visible(show_cell_settings)
self._apn_btn.set_visible(show_cell_settings)
self._cellular_metered_btn.set_visible(show_cell_settings)
def show_event(self):
super().show_event()
self._wifi_manager.set_active(True)
# Process wifi callbacks while at any point in the nav stack
gui_app.add_nav_stack_tick(self._wifi_manager.process_callbacks)
def hide_event(self):
super().hide_event()
self._wifi_manager.set_active(False)
gui_app.remove_nav_stack_tick(self._wifi_manager.process_callbacks)
def _toggle_roaming(self, checked: bool):
self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered"))
def _edit_apn(self):
def update_apn(apn: str):
apn = apn.strip()
if apn == "":
ui_state.params.remove("GsmApn")
else:
ui_state.params.put("GsmApn", apn)
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), apn, ui_state.params.get_bool("GsmMetered"))
current_apn = ui_state.params.get("GsmApn") or ""
dlg = BigInputDialog("enter APN...", current_apn, minimum_length=0, confirm_callback=update_apn)
gui_app.push_widget(dlg)
def _toggle_cellular_metered(self, checked: bool):
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), ui_state.params.get("GsmApn") or "", checked)
def _on_network_updated(self, networks: list[Network]):
# Update tethering state
tethering_active = self._wifi_manager.is_tethering_active()
# TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons
self._tethering_toggle_btn.set_enabled(True)
self._tethering_password_btn.set_enabled(True)
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
self._tethering_toggle_btn.set_checked(tethering_active)
# Update network metered
self._network_metered_btn.set_value(
{
MeteredType.UNKNOWN: 'default',
MeteredType.YES: 'metered',
MeteredType.NO: 'unmetered'
}.get(self._wifi_manager.current_network_metered, 'default'))
@@ -1,154 +0,0 @@
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'))
@@ -3,8 +3,9 @@ import numpy as np
import pyray as rl import pyray as rl
from collections.abc import Callable from collections.abc import Callable
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog, BigConfirmationDialog from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog, BigConfirmationDialogV2
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, LABEL_COLOR from openpilot.selfdrive.ui.mici.widgets.button import BigButton, LABEL_COLOR
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
@@ -13,26 +14,39 @@ from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityT
class LoadingAnimation(Widget): class LoadingAnimation(Widget):
RADIUS = 8 HIDE_TIME = 4
SPACING = 24 # center-to-center: diameter (16) + gap (8)
Y_MAG = 11.2
def __init__(self): def __init__(self):
super().__init__() super().__init__()
w = self.SPACING * 2 + self.RADIUS * 2 self._opacity_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
h = self.RADIUS * 2 + int(self.Y_MAG) self._opacity_target = 1.0
self.set_rect(rl.Rectangle(0, 0, w, h)) self._hide_time = 0.0
def show_event(self):
self._opacity_target = 1.0
self._hide_time = rl.get_time()
def _render(self, _): def _render(self, _):
# Balls rest at bottom center; bounce upward if rl.get_time() - self._hide_time > self.HIDE_TIME:
base_x = int(self._rect.x + self._rect.width / 2) self._opacity_target = 0.0
base_y = int(self._rect.y + self._rect.height - self.RADIUS)
self._opacity_filter.update(self._opacity_target)
if self._opacity_filter.x < 0.01:
return
cx = int(self._rect.x + self._rect.width / 2)
cy = int(self._rect.y + self._rect.height / 2)
y_mag = 7
anim_scale = 4
spacing = 14
for i in range(3): for i in range(3):
x = base_x + (i - 1) * self.SPACING x = cx - spacing + i * spacing
y = int(base_y + min(math.sin((rl.get_time() - i * 0.2) * 4) * self.Y_MAG, 0)) y = int(cy + min(math.sin((rl.get_time() - i * 0.2) * anim_scale) * y_mag, 0))
alpha = int(np.interp(base_y - y, [0, self.Y_MAG], [255 * 0.45, 255 * 0.9])) alpha = int(np.interp(cy - y, [0, y_mag], [255 * 0.45, 255 * 0.9]) * self._opacity_filter.x)
rl.draw_circle(x, y, self.RADIUS, rl.Color(255, 255, 255, alpha)) rl.draw_circle(x, y, 5, rl.Color(255, 255, 255, alpha))
class WifiIcon(Widget): class WifiIcon(Widget):
@@ -110,10 +124,6 @@ class WifiButton(BigButton):
if self._is_connected or self._is_connecting: if self._is_connected or self._is_connecting:
self._wrong_password = False self._wrong_password = False
@property
def network_forgetting(self) -> bool:
return self._network_forgetting
def _forget_network(self): def _forget_network(self):
if self._network_forgetting: if self._network_forgetting:
return return
@@ -165,7 +175,7 @@ class WifiButton(BigButton):
if self._is_connected and not self._network_forgetting: if self._is_connected and not self._network_forgetting:
check_y = int(label_y - sub_label_height + (sub_label_height - self._check_txt.height) / 2) check_y = int(label_y - sub_label_height + (sub_label_height - self._check_txt.height) / 2)
rl.draw_texture_ex(self._check_txt, rl.Vector2(sub_label_x, check_y), 0.0, 1.0, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))) rl.draw_texture(self._check_txt, int(sub_label_x), check_y, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)))
sub_label_x += self._check_txt.width + 14 sub_label_x += self._check_txt.width + 14
sub_label_rect = rl.Rectangle(sub_label_x, label_y - sub_label_height, sub_label_w, sub_label_height) sub_label_rect = rl.Rectangle(sub_label_x, label_y - sub_label_height, sub_label_w, sub_label_height)
@@ -246,7 +256,8 @@ class ForgetButton(Widget):
def _handle_mouse_release(self, mouse_pos: MousePos): def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos) super()._handle_mouse_release(mouse_pos)
dlg = BigConfirmationDialog("slide to forget", gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64), self._forget_network, red=True) dlg = BigConfirmationDialogV2("slide to forget", "icons_mici/settings/network/new/trash.png", red=True,
confirm_callback=self._forget_network)
gui_app.push_widget(dlg) gui_app.push_widget(dlg)
def _render(self, _): def _render(self, _):
@@ -259,26 +270,11 @@ class ForgetButton(Widget):
rl.draw_texture_ex(self._trash_txt, (trash_x, trash_y), 0, 1.0, rl.WHITE) rl.draw_texture_ex(self._trash_txt, (trash_x, trash_y), 0, 1.0, rl.WHITE)
class ScanningButton(BigButton):
def __init__(self):
super().__init__("", "searching for networks")
self.set_enabled(False)
self._loading_animation = LoadingAnimation()
def _draw_content(self, btn_y: float):
super()._draw_content(btn_y)
anim = self._loading_animation
x = self._rect.x + self._rect.width - anim.rect.width - 40
y = btn_y + self._rect.height - anim.rect.height - 30
anim.set_position(x, y)
anim.render()
class WifiUIMici(NavScroller): class WifiUIMici(NavScroller):
def __init__(self, wifi_manager: WifiManager): def __init__(self, wifi_manager: WifiManager):
super().__init__() super().__init__()
self._scanning_btn = ScanningButton() self._loading_animation = LoadingAnimation()
self._wifi_manager = wifi_manager self._wifi_manager = wifi_manager
self._networks: dict[str, Network] = {} self._networks: dict[str, Network] = {}
@@ -289,23 +285,20 @@ class WifiUIMici(NavScroller):
networks_updated=self._on_network_updated, networks_updated=self._on_network_updated,
) )
@property
def any_network_forgetting(self) -> bool:
# TODO: deactivate before forget and add DISCONNECTING state
return any(btn.network_forgetting for btn in self._scroller.items if isinstance(btn, WifiButton))
def show_event(self): def show_event(self):
# Re-sort scroller items and update from latest scan results # Clear scroller items and update from latest scan results
super().show_event() super().show_event()
self._loading_animation.show_event()
self._wifi_manager.set_active(True) self._wifi_manager.set_active(True)
self._networks = {n.ssid: n for n in self._wifi_manager.networks} self._scroller.items.clear()
self._update_buttons(re_sort=True) # trigger button update on latest sorted networks
self._on_network_updated(self._wifi_manager.networks)
def _on_network_updated(self, networks: list[Network]): def _on_network_updated(self, networks: list[Network]):
self._networks = {network.ssid: network for network in networks} self._networks = {network.ssid: network for network in networks}
self._update_buttons() self._update_buttons()
def _update_buttons(self, re_sort: bool = False): def _update_buttons(self):
# Update existing buttons, add new ones to the end # Update existing buttons, add new ones to the end
existing = {btn.network.ssid: btn for btn in self._scroller.items if isinstance(btn, WifiButton)} existing = {btn.network.ssid: btn for btn in self._scroller.items if isinstance(btn, WifiButton)}
@@ -317,22 +310,10 @@ class WifiUIMici(NavScroller):
btn.set_click_callback(lambda ssid=network.ssid: self._connect_to_network(ssid)) btn.set_click_callback(lambda ssid=network.ssid: self._connect_to_network(ssid))
self._scroller.add_widget(btn) self._scroller.add_widget(btn)
if re_sort: # Mark networks no longer in scan results (display handled by _update_state)
# Remove stale buttons and sort to match scan order, preserving eager state for btn in self._scroller.items:
btn_map = {btn.network.ssid: btn for btn in self._scroller.items if isinstance(btn, WifiButton)} if isinstance(btn, WifiButton) and btn.network.ssid not in self._networks:
self._scroller.items[:] = [btn_map[ssid] for ssid in self._networks if ssid in btn_map] btn.set_network_missing(True)
else:
# Mark networks no longer in scan results (display handled by _update_state)
for btn in self._scroller.items:
if isinstance(btn, WifiButton) and btn.network.ssid not in self._networks:
btn.set_network_missing(True)
# Keep scanning button at the end
items = self._scroller.items
if self._scanning_btn in items:
items.append(items.pop(items.index(self._scanning_btn)))
else:
self._scroller.add_widget(self._scanning_btn)
def _connect_with_password(self, ssid: str, password: str): def _connect_with_password(self, ssid: str, password: str):
self._wifi_manager.connect_to_network(ssid, password) self._wifi_manager.connect_to_network(ssid, password)
@@ -389,3 +370,17 @@ class WifiUIMici(NavScroller):
super()._update_state() super()._update_state()
self._move_network_to_front(self._wifi_manager.wifi_state.ssid) self._move_network_to_front(self._wifi_manager.wifi_state.ssid)
# Show loading animation near end
max_scroll = max(self._scroller.content_size - self._scroller.rect.width, 1)
progress = -self._scroller.scroll_panel.get_offset() / max_scroll
if progress > 0.8 or len(self._scroller.items) <= 1:
self._loading_animation.show_event()
def _render(self, _):
super()._render(self._rect)
anim_w = 90
anim_x = self._rect.x + self._rect.width - anim_w
anim_y = self._rect.y + self._rect.height - 25 + 2
self._loading_animation.render(rl.Rectangle(anim_x, anim_y, anim_w, 20))
@@ -2,7 +2,7 @@ from openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller import NavScroller from openpilot.system.ui.widgets.scroller import NavScroller
from openpilot.selfdrive.ui.mici.widgets.button import BigButton from openpilot.selfdrive.ui.mici.widgets.button import BigButton
from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.network.network_layout import NetworkLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.network import NetworkLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton
from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout
@@ -20,23 +20,23 @@ class SettingsLayout(NavScroller):
self._params = Params() self._params = Params()
toggles_panel = TogglesLayoutMici() toggles_panel = TogglesLayoutMici()
toggles_btn = SettingsBigButton("toggles", "", gui_app.texture("icons_mici/settings.png", 64, 64)) toggles_btn = SettingsBigButton("toggles", "", "icons_mici/settings.png")
toggles_btn.set_click_callback(lambda: gui_app.push_widget(toggles_panel)) toggles_btn.set_click_callback(lambda: gui_app.push_widget(toggles_panel))
network_panel = NetworkLayoutMici() network_panel = NetworkLayoutMici()
network_btn = SettingsBigButton("network", "", gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 76, 56)) network_btn = SettingsBigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png", icon_size=(76, 56))
network_btn.set_click_callback(lambda: gui_app.push_widget(network_panel)) network_btn.set_click_callback(lambda: gui_app.push_widget(network_panel))
device_panel = DeviceLayoutMici() device_panel = DeviceLayoutMici()
device_btn = SettingsBigButton("device", "", gui_app.texture("icons_mici/settings/device_icon.png", 72, 58)) device_btn = SettingsBigButton("device", "", "icons_mici/settings/device_icon.png", icon_size=(74, 60))
device_btn.set_click_callback(lambda: gui_app.push_widget(device_panel)) device_btn.set_click_callback(lambda: gui_app.push_widget(device_panel))
developer_panel = DeveloperLayoutMici() developer_panel = DeveloperLayoutMici()
developer_btn = SettingsBigButton("developer", "", gui_app.texture("icons_mici/settings/developer_icon.png", 64, 60)) developer_btn = SettingsBigButton("developer", "", "icons_mici/settings/developer_icon.png", icon_size=(64, 60))
developer_btn.set_click_callback(lambda: gui_app.push_widget(developer_panel)) developer_btn.set_click_callback(lambda: gui_app.push_widget(developer_panel))
firehose_panel = FirehoseLayout() firehose_panel = FirehoseLayout()
firehose_btn = SettingsBigButton("firehose", "", gui_app.texture("icons_mici/settings/firehose.png", 52, 62)) firehose_btn = SettingsBigButton("firehose", "", "icons_mici/settings/firehose.png", icon_size=(52, 62))
firehose_btn.set_click_callback(lambda: gui_app.push_widget(firehose_panel)) firehose_btn.set_click_callback(lambda: gui_app.push_widget(firehose_panel))
self._scroller.add_widgets([ self._scroller.add_widgets([
+3 -3
View File
@@ -231,7 +231,7 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
self._alpha_filter.update(0 if alert is None else 1) self._alpha_filter.update(0 if alert is None else 1)
if gui_app.sunnypilot_ui(): if gui_app.sunnypilot_ui():
ui_state.onroad_brightness_handle_alerts(ui_state, alert) ui_state.onroad_brightness_handle_alerts(ui_state.started, alert)
if alert is None: if alert is None:
# If still animating out, keep the previous alert # If still animating out, keep the previous alert
@@ -272,8 +272,8 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
else: else:
icon_alpha = int(min(self._turn_signal_alpha_filter.x, 255)) icon_alpha = int(min(self._turn_signal_alpha_filter.x, 255))
rl.draw_texture_ex(alert_layout.icon.texture, rl.Vector2(pos_x, self._rect.y + alert_layout.icon.margin_y), 0.0, 1.0, rl.draw_texture(alert_layout.icon.texture, pos_x, int(self._rect.y + alert_layout.icon.margin_y),
rl.Color(255, 255, 255, int(icon_alpha * self._alpha_filter.x))) rl.Color(255, 255, 255, int(icon_alpha * self._alpha_filter.x)))
def _draw_background(self, alert: Alert) -> None: def _draw_background(self, alert: Alert) -> None:
# draw top gradient for alert text at top # draw top gradient for alert text at top
@@ -130,7 +130,7 @@ class BookmarkIcon(Widget):
if self._offset_filter.x > 0: if self._offset_filter.x > 0:
icon_x = self.rect.x + self.rect.width - round(self._offset_filter.x) icon_x = self.rect.x + self.rect.width - round(self._offset_filter.x)
icon_y = self.rect.y + (self.rect.height - self._icon.height) / 2 # Vertically centered icon_y = self.rect.y + (self.rect.height - self._icon.height) / 2 # Vertically centered
rl.draw_texture_ex(self._icon, rl.Vector2(icon_x, icon_y), 0.0, 1.0, rl.WHITE) rl.draw_texture(self._icon, int(icon_x), int(icon_y), rl.WHITE)
class AugmentedRoadView(CameraView): class AugmentedRoadView(CameraView):
@@ -251,7 +251,7 @@ class AugmentedRoadView(CameraView):
# Draw darkened background and text if not onroad # Draw darkened background and text if not onroad
if not ui_state.started: if not ui_state.started:
rl.draw_rectangle(int(self.rect.x), int(self.rect.y), int(self.rect.width), int(self.rect.height), rl.Color(0, 0, 0, 175)) rl.draw_rectangle(int(self.rect.x), int(self.rect.y), int(self.rect.width), int(self.rect.height), rl.Color(0, 0, 0, 175))
self._offroad_label.render(self._rect) self._offroad_label.render(self._content_rect)
# publish uiDebug # publish uiDebug
msg = messaging.new_message('uiDebug') msg = messaging.new_message('uiDebug')
+1 -1
View File
@@ -155,11 +155,11 @@ class CameraView(Widget):
# Prevent old frames from showing when going onroad. Qt has a separate thread # Prevent old frames from showing when going onroad. Qt has a separate thread
# which drains the VisionIpcClient SubSocket for us. Re-connecting is not enough # which drains the VisionIpcClient SubSocket for us. Re-connecting is not enough
# and only clears internal buffers, not the message queue. # and only clears internal buffers, not the message queue.
self.frame = None
self.available_streams.clear() self.available_streams.clear()
if self.client: if self.client:
del self.client del self.client
self.client = VisionIpcClient(self._name, self._stream_type, conflate=True) self.client = VisionIpcClient(self._name, self._stream_type, conflate=True)
self.frame = None
def _set_placeholder_color(self, color: rl.Color): def _set_placeholder_color(self, color: rl.Color):
"""Set a placeholder color to be drawn when no frame is available.""" """Set a placeholder color to be drawn when no frame is available."""
+9 -8
View File
@@ -61,7 +61,7 @@ class DriverStateRenderer(Widget):
self._dm_cone = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_cone.png", cone_and_person_size, cone_and_person_size) self._dm_cone = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_cone.png", cone_and_person_size, cone_and_person_size)
center_size = round(36 / self.BASE_SIZE * self._rect.width) center_size = round(36 / self.BASE_SIZE * self._rect.width)
self._dm_center = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_center.png", center_size, center_size) self._dm_center = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_center.png", center_size, center_size)
self._dm_background = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_background.png", int(self._rect.width), int(self._rect.height)) self._dm_background = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_background.png", self._rect.width, self._rect.height)
def set_should_draw(self, should_draw: bool): def set_should_draw(self, should_draw: bool):
self._should_draw = should_draw self._should_draw = should_draw
@@ -88,14 +88,15 @@ class DriverStateRenderer(Widget):
if DEBUG: if DEBUG:
rl.draw_rectangle_lines_ex(self._rect, 1, rl.RED) rl.draw_rectangle_lines_ex(self._rect, 1, rl.RED)
rl.draw_texture_ex(self._dm_background, rl.draw_texture(self._dm_background,
rl.Vector2(self._rect.x, self._rect.y), 0.0, 1.0, int(self._rect.x),
rl.Color(255, 255, 255, int(255 * self._fade_filter.x))) int(self._rect.y),
rl.Color(255, 255, 255, int(255 * self._fade_filter.x)))
rl.draw_texture_ex(self._dm_person, rl.draw_texture(self._dm_person,
rl.Vector2(self._rect.x + (self._rect.width - self._dm_person.width) / 2, int(self._rect.x + (self._rect.width - self._dm_person.width) / 2),
self._rect.y + (self._rect.height - self._dm_person.height) / 2), 0.0, 1.0, int(self._rect.y + (self._rect.height - self._dm_person.height) / 2),
rl.Color(255, 255, 255, int(255 * 0.9 * self._fade_filter.x))) rl.Color(255, 255, 255, int(255 * 0.9 * self._fade_filter.x)))
if self.effective_active: if self.effective_active:
source_rect = rl.Rectangle(0, 0, self._dm_cone.width, self._dm_cone.height) source_rect = rl.Rectangle(0, 0, self._dm_cone.width, self._dm_cone.height)
+3 -2
View File
@@ -172,7 +172,8 @@ class HudRenderer(Widget):
def _render(self, rect: rl.Rectangle) -> None: def _render(self, rect: rl.Rectangle) -> None:
"""Render HUD elements to the screen.""" """Render HUD elements to the screen."""
self._torque_bar.render(rect) if ui_state.sm['controlsState'].lateralControlState.which() != 'angleState':
self._torque_bar.render(rect)
if self.is_cruise_set: if self.is_cruise_set:
self._draw_set_speed(rect) self._draw_set_speed(rect)
@@ -221,7 +222,7 @@ class HudRenderer(Widget):
EXCLAMATION_POINT_SPACING = 10 EXCLAMATION_POINT_SPACING = 10
exclamation_pos_x = pos_x - self._txt_exclamation_point.width / 2 + wheel_txt.width / 2 + EXCLAMATION_POINT_SPACING exclamation_pos_x = pos_x - self._txt_exclamation_point.width / 2 + wheel_txt.width / 2 + EXCLAMATION_POINT_SPACING
exclamation_pos_y = pos_y - self._txt_exclamation_point.height / 2 exclamation_pos_y = pos_y - self._txt_exclamation_point.height / 2
rl.draw_texture_ex(self._txt_exclamation_point, rl.Vector2(exclamation_pos_x, exclamation_pos_y), 0.0, 1.0, rl.WHITE) rl.draw_texture(self._txt_exclamation_point, int(exclamation_pos_x), int(exclamation_pos_y), rl.WHITE)
def _draw_set_speed(self, rect: rl.Rectangle) -> None: def _draw_set_speed(self, rect: rl.Rectangle) -> None:
"""Draw the MAX speed indicator box.""" """Draw the MAX speed indicator box."""
+5 -15
View File
@@ -145,9 +145,6 @@ def arc_bar_pts(cx: float, cy: float,
return pts return pts
DEFAULT_MAX_LAT_ACCEL = 3.0 # m/s^2
class TorqueBar(Widget): class TorqueBar(Widget):
def __init__(self, demo: bool = False, scale: float = 1.0, always: bool = False): def __init__(self, demo: bool = False, scale: float = 1.0, always: bool = False):
super().__init__() super().__init__()
@@ -170,23 +167,16 @@ class TorqueBar(Widget):
controls_state = ui_state.sm['controlsState'] controls_state = ui_state.sm['controlsState']
car_state = ui_state.sm['carState'] car_state = ui_state.sm['carState']
live_parameters = ui_state.sm['liveParameters'] live_parameters = ui_state.sm['liveParameters']
car_control = ui_state.sm['carControl'] lateral_acceleration = controls_state.curvature * car_state.vEgo ** 2 - live_parameters.roll * ACCELERATION_DUE_TO_GRAVITY
# TODO: pull from carparams
max_lateral_acceleration = 3
# Include lateral accel error in estimated torque utilization # from selfdrived
actual_lateral_accel = controls_state.curvature * car_state.vEgo ** 2 actual_lateral_accel = controls_state.curvature * car_state.vEgo ** 2
desired_lateral_accel = controls_state.desiredCurvature * car_state.vEgo ** 2 desired_lateral_accel = controls_state.desiredCurvature * car_state.vEgo ** 2
accel_diff = (desired_lateral_accel - actual_lateral_accel) accel_diff = (desired_lateral_accel - actual_lateral_accel)
# Include road roll in estimated torque utilization self._torque_filter.update(min(max(lateral_acceleration / max_lateral_acceleration + accel_diff, -1), 1))
# Roll is less accurate near standstill, so reduce its effect at low speed
roll_compensation = live_parameters.roll * ACCELERATION_DUE_TO_GRAVITY * np.interp(car_state.vEgo, [5, 15], [0.0, 1.0])
lateral_acceleration = actual_lateral_accel - roll_compensation
max_lateral_acceleration = ui_state.CP.maxLateralAccel if ui_state.CP else DEFAULT_MAX_LAT_ACCEL
if not car_control.latActive:
self._torque_filter.update(0.0)
else:
self._torque_filter.update(np.clip((lateral_acceleration + accel_diff) / max_lateral_acceleration, -1, 1))
else: else:
self._torque_filter.update(-ui_state.sm['carOutput'].actuatorsOutput.torque) self._torque_filter.update(-ui_state.sm['carOutput'].actuatorsOutput.torque)
+4 -6
View File
@@ -10,7 +10,7 @@ from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2, BigInputDialog
from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal
# tici dialogs # tici dialogs
@@ -44,7 +44,7 @@ KNOWN_LEAKS = {
"openpilot.system.ui.widgets.scroller_tici.Scroller", "openpilot.system.ui.widgets.scroller_tici.Scroller",
"openpilot.system.ui.widgets.label.UnifiedLabel", "openpilot.system.ui.widgets.label.UnifiedLabel",
"openpilot.system.ui.widgets.mici_keyboard.MiciKeyboard", "openpilot.system.ui.widgets.mici_keyboard.MiciKeyboard",
"openpilot.selfdrive.ui.mici.widgets.dialog.BigConfirmationDialog", "openpilot.selfdrive.ui.mici.widgets.dialog.BigConfirmationDialogV2",
"openpilot.system.ui.widgets.keyboard.Keyboard", "openpilot.system.ui.widgets.keyboard.Keyboard",
"openpilot.system.ui.widgets.slider.BigSlider", "openpilot.system.ui.widgets.slider.BigSlider",
"openpilot.selfdrive.ui.mici.widgets.dialog.BigInputDialog", "openpilot.selfdrive.ui.mici.widgets.dialog.BigInputDialog",
@@ -68,11 +68,9 @@ def test_dialogs_do_not_leak():
for ctor in ( for ctor in (
# mici # mici
MiciDriverCameraDialog, MiciPairingDialog, MiciDriverCameraDialog, MiciTrainingGuide, MiciOnboardingWindow, MiciPairingDialog,
lambda: MiciTrainingGuide(lambda: None),
lambda: MiciOnboardingWindow(lambda: None),
lambda: BigDialog("test", "test"), lambda: BigDialog("test", "test"),
lambda: BigConfirmationDialog("test", gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64), lambda: None), lambda: BigConfirmationDialogV2("test", "icons_mici/settings/network/new/trash.png"),
lambda: BigInputDialog("test"), lambda: BigInputDialog("test"),
lambda: MiciFccModal(text="test"), lambda: MiciFccModal(text="test"),
# tici # tici
+15 -63
View File
@@ -28,7 +28,7 @@ class ScrollState(Enum):
class BigCircleButton(Widget): class BigCircleButton(Widget):
def __init__(self, icon: rl.Texture, red: bool = False, icon_offset: tuple[int, int] = (0, 0)): def __init__(self, icon: str, red: bool = False, icon_size: tuple[int, int] = (64, 53), icon_offset: tuple[int, int] = (0, 0)):
super().__init__() super().__init__()
self._red = red self._red = red
self._icon_offset = icon_offset self._icon_offset = icon_offset
@@ -39,7 +39,7 @@ class BigCircleButton(Widget):
self._click_delay = 0.075 self._click_delay = 0.075
# Icons # Icons
self._txt_icon = icon self._txt_icon = gui_app.texture(icon, *icon_size)
self._txt_btn_disabled_bg = gui_app.texture("icons_mici/buttons/button_circle_disabled.png", 180, 180) self._txt_btn_disabled_bg = gui_app.texture("icons_mici/buttons/button_circle_disabled.png", 180, 180)
self._txt_btn_bg = gui_app.texture("icons_mici/buttons/button_circle.png", 180, 180) self._txt_btn_bg = gui_app.texture("icons_mici/buttons/button_circle.png", 180, 180)
@@ -71,8 +71,8 @@ class BigCircleButton(Widget):
class BigCircleToggle(BigCircleButton): class BigCircleToggle(BigCircleButton):
def __init__(self, icon: rl.Texture, toggle_callback: Callable | None = None, icon_offset: tuple[int, int] = (0, 0)): def __init__(self, icon: str, toggle_callback: Callable | None = None, icon_size: tuple[int, int] = (64, 53), icon_offset: tuple[int, int] = (0, 0)):
super().__init__(icon, False, icon_offset=icon_offset) super().__init__(icon, False, icon_size=icon_size, icon_offset=icon_offset)
self._toggle_callback = toggle_callback self._toggle_callback = toggle_callback
# State # State
@@ -107,18 +107,19 @@ class BigButton(Widget):
"""A lightweight stand-in for the Qt BigButton, drawn & updated each frame.""" """A lightweight stand-in for the Qt BigButton, drawn & updated each frame."""
def __init__(self, text: str, value: str = "", icon: Union[rl.Texture, None] = None, scroll: bool = False): def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", icon_size: tuple[int, int] = (64, 64),
scroll: bool = False):
super().__init__() super().__init__()
self.set_rect(rl.Rectangle(0, 0, 402, 180)) self.set_rect(rl.Rectangle(0, 0, 402, 180))
self.text = text self.text = text
self.value = value self.value = value
self._txt_icon = icon self._icon_size = icon_size
self._scroll = scroll self._scroll = scroll
self.set_icon(icon)
self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps) self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps)
self._click_delay = 0.075 self._click_delay = 0.075
self._shake_start: float | None = None self._shake_start: float | None = None
self._grow_animation_until: float | None = None
self._rotate_icon_t: float | None = None self._rotate_icon_t: float | None = None
@@ -131,8 +132,8 @@ class BigButton(Widget):
self._load_images() self._load_images()
def set_icon(self, icon: Union[rl.Texture, None]): def set_icon(self, icon: Union[str, rl.Texture]):
self._txt_icon = icon self._txt_icon = gui_app.texture(icon, *self._icon_size) if isinstance(icon, str) and len(icon) else icon
def set_rotate_icon(self, rotate: bool): def set_rotate_icon(self, rotate: bool):
if rotate and self._rotate_icon_t is not None: if rotate and self._rotate_icon_t is not None:
@@ -144,12 +145,9 @@ class BigButton(Widget):
self._txt_pressed_bg = gui_app.texture("icons_mici/buttons/button_rectangle_pressed.png", 402, 180) self._txt_pressed_bg = gui_app.texture("icons_mici/buttons/button_rectangle_pressed.png", 402, 180)
self._txt_disabled_bg = gui_app.texture("icons_mici/buttons/button_rectangle_disabled.png", 402, 180) self._txt_disabled_bg = gui_app.texture("icons_mici/buttons/button_rectangle_disabled.png", 402, 180)
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
super().set_touch_valid_callback(lambda: touch_callback() and self._grow_animation_until is None)
def _width_hint(self) -> int: def _width_hint(self) -> int:
# Single line if scrolling, so hide behind icon if exists # Single line if scrolling, so hide behind icon if exists
icon_size = self._txt_icon.width if self._txt_icon and self._scroll and self.value else 0 icon_size = self._icon_size[0] if self._txt_icon and self._scroll and self.value else 0
return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2 - icon_size) return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2 - icon_size)
def _get_label_font_size(self): def _get_label_font_size(self):
@@ -184,17 +182,12 @@ class BigButton(Widget):
def trigger_shake(self): def trigger_shake(self):
self._shake_start = rl.get_time() self._shake_start = rl.get_time()
def trigger_grow_animation(self, duration: float = 0.65):
self._grow_animation_until = rl.get_time() + duration
@property @property
def _shake_offset(self) -> float: def _shake_offset(self) -> float:
SHAKE_DURATION = 0.5 SHAKE_DURATION = 0.5
SHAKE_AMPLITUDE = 24.0 SHAKE_AMPLITUDE = 24.0
SHAKE_FREQUENCY = 32.0 SHAKE_FREQUENCY = 32.0
if self._shake_start is None: t = rl.get_time() - (self._shake_start or 0.0)
return 0.0
t = rl.get_time() - self._shake_start
if t > SHAKE_DURATION: if t > SHAKE_DURATION:
return 0.0 return 0.0
decay = 1.0 - t / SHAKE_DURATION decay = 1.0 - t / SHAKE_DURATION
@@ -204,10 +197,6 @@ class BigButton(Widget):
super().set_position(x + self._shake_offset, y) super().set_position(x + self._shake_offset, y)
def _handle_background(self) -> tuple[rl.Texture, float, float, float]: def _handle_background(self) -> tuple[rl.Texture, float, float, float]:
if self._grow_animation_until is not None:
if rl.get_time() >= self._grow_animation_until:
self._grow_animation_until = None
# draw _txt_default_bg # draw _txt_default_bg
txt_bg = self._txt_default_bg txt_bg = self._txt_default_bg
if not self.enabled: if not self.enabled:
@@ -215,7 +204,7 @@ class BigButton(Widget):
elif self.is_pressed: elif self.is_pressed:
txt_bg = self._txt_pressed_bg txt_bg = self._txt_pressed_bg
scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed or self._grow_animation_until is not None else 1.0) scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed else 1.0)
btn_x = self._rect.x + (self._rect.width * (1 - scale)) / 2 btn_x = self._rect.x + (self._rect.width * (1 - scale)) / 2
btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2 btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2
return txt_bg, btn_x, btn_y, scale return txt_bg, btn_x, btn_y, scale
@@ -335,43 +324,6 @@ class BigMultiToggle(BigToggle):
y += 35 y += 35
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 _get_label_font_size(self):
return 36
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 BigMultiParamToggle(BigMultiToggle): class BigMultiParamToggle(BigMultiToggle):
def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable | None = None, def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable | None = None,
select_callback: Callable | None = None): select_callback: Callable | None = None):
@@ -407,9 +359,9 @@ class BigParamControl(BigToggle):
# TODO: param control base class # TODO: param control base class
class BigCircleParamControl(BigCircleToggle): class BigCircleParamControl(BigCircleToggle):
def __init__(self, icon: rl.Texture, param: str, toggle_callback: Callable | None = None, def __init__(self, icon: str, param: str, toggle_callback: Callable | None = None, icon_size: tuple[int, int] = (64, 53),
icon_offset: tuple[int, int] = (0, 0)): icon_offset: tuple[int, int] = (0, 0)):
super().__init__(icon, toggle_callback, icon_offset=icon_offset) super().__init__(icon, toggle_callback, icon_size=icon_size, icon_offset=icon_offset)
self._param = param self._param = param
self.params = Params() self.params = Params()
self.set_checked(self.params.get_bool(self._param, False)) self.set_checked(self.params.get_bool(self._param, False))
+54 -39
View File
@@ -4,13 +4,14 @@ import pyray as rl
from typing import Union from typing import Union
from collections.abc import Callable from collections.abc import Callable
from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.widgets.nav_widget import NavWidget
from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
from openpilot.system.ui.widgets.mici_keyboard import MiciKeyboard from openpilot.system.ui.widgets.mici_keyboard import MiciKeyboard
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.widgets.slider import RedBigSlider, BigSlider from openpilot.system.ui.widgets.slider import RedBigSlider, BigSlider
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton, BigButton, GreyBigButton from openpilot.selfdrive.ui.mici.widgets.button import BigButton
DEBUG = False DEBUG = False
@@ -24,31 +25,58 @@ class BigDialogBase(NavWidget, abc.ABC):
class BigDialog(BigDialogBase): class BigDialog(BigDialogBase):
def __init__(self, title: str, description: str, icon: Union[rl.Texture, None] = None): def __init__(self,
title: str,
description: str):
super().__init__() super().__init__()
self._card = GreyBigButton(title, description, icon) self._title = title
self._description = description
def _render(self, _): def _render(self, _):
self._card.render(rl.Rectangle( super()._render(_)
self._rect.x + self._rect.width / 2 - self._card.rect.width / 2,
self._rect.y + self._rect.height / 2 - self._card.rect.height / 2, # draw title
self._card.rect.width, # TODO: we desperately need layouts
self._card.rect.height, # TODO: coming up with these numbers manually is a pain and not scalable
)) # TODO: no clue what any of these numbers mean. VBox and HBox would remove all of this shite
max_width = self._rect.width - PADDING * 2
title_wrapped = '\n'.join(wrap_text(gui_app.font(FontWeight.BOLD), self._title, 50, int(max_width)))
title_size = measure_text_cached(gui_app.font(FontWeight.BOLD), title_wrapped, 50)
text_x_offset = 0
title_rect = rl.Rectangle(int(self._rect.x + text_x_offset + PADDING),
int(self._rect.y + PADDING),
int(max_width),
int(title_size.y))
gui_label(title_rect, title_wrapped, 50, font_weight=FontWeight.BOLD,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
# draw description
desc_wrapped = '\n'.join(wrap_text(gui_app.font(FontWeight.MEDIUM), self._description, 30, int(max_width)))
desc_size = measure_text_cached(gui_app.font(FontWeight.MEDIUM), desc_wrapped, 30)
desc_rect = rl.Rectangle(int(self._rect.x + text_x_offset + PADDING),
int(self._rect.y + self._rect.height / 3),
int(max_width),
int(desc_size.y))
# TODO: text align doesn't seem to work properly with newlines
gui_label(desc_rect, desc_wrapped, 30, font_weight=FontWeight.MEDIUM,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
class BigConfirmationDialog(BigDialogBase): class BigConfirmationDialogV2(BigDialogBase):
def __init__(self, title: str, icon: rl.Texture, confirm_callback: Callable[[], None], def __init__(self, title: str, icon: str, red: bool = False,
exit_on_confirm: bool = True, red: bool = False): exit_on_confirm: bool = True,
confirm_callback: Callable | None = None):
super().__init__() super().__init__()
self._confirm_callback = confirm_callback self._confirm_callback = confirm_callback
self._exit_on_confirm = exit_on_confirm self._exit_on_confirm = exit_on_confirm
icon_txt = gui_app.texture(icon, 64, 53)
self._slider: BigSlider | RedBigSlider self._slider: BigSlider | RedBigSlider
if red: if red:
self._slider = self._child(RedBigSlider(title, icon, confirm_callback=self._on_confirm)) self._slider = RedBigSlider(title, icon_txt, confirm_callback=self._on_confirm)
else: else:
self._slider = self._child(BigSlider(title, icon, confirm_callback=self._on_confirm)) self._slider = BigSlider(title, icon_txt, confirm_callback=self._on_confirm)
self._slider.set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack + NavWidget self._slider.set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack + NavWidget
def _on_confirm(self): def _on_confirm(self):
@@ -75,12 +103,11 @@ class BigInputDialog(BigDialogBase):
hint: str, hint: str,
default_text: str = "", default_text: str = "",
minimum_length: int = 1, minimum_length: int = 1,
confirm_callback: Callable[[str], None] | None = None, confirm_callback: Callable[[str], None] | None = None):
auto_return_to_letters: str = ""):
super().__init__() super().__init__()
self._hint_label = UnifiedLabel(hint, font_size=35, text_color=rl.Color(255, 255, 255, int(255 * 0.35)), self._hint_label = UnifiedLabel(hint, font_size=35, text_color=rl.Color(255, 255, 255, int(255 * 0.35)),
font_weight=FontWeight.MEDIUM) font_weight=FontWeight.MEDIUM)
self._keyboard = MiciKeyboard(auto_return_to_letters=auto_return_to_letters) self._keyboard = MiciKeyboard()
self._keyboard.set_text(default_text) self._keyboard.set_text(default_text)
self._keyboard.set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack + NavWidget self._keyboard.set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack + NavWidget
self._minimum_length = minimum_length self._minimum_length = minimum_length
@@ -130,9 +157,9 @@ class BigInputDialog(BigDialogBase):
bg_block_margin = 5 bg_block_margin = 5
text_x = PADDING / 2 + self._enter_img.width + PADDING text_x = PADDING / 2 + self._enter_img.width + PADDING
text_field_rect = rl.Rectangle(text_x, self._rect.y + PADDING - bg_block_margin, text_field_rect = rl.Rectangle(text_x, int(self._rect.y + PADDING) - bg_block_margin,
self._rect.width - text_x * 2, int(self._rect.width - text_x * 2),
text_size.y) int(text_size.y))
# draw text input # draw text input
# push text left with a gradient on left side if too long # push text left with a gradient on left side if too long
@@ -153,8 +180,8 @@ class BigInputDialog(BigDialogBase):
# draw gradient on left side to indicate more text # draw gradient on left side to indicate more text
if text_size.x > text_field_rect.width: if text_size.x > text_field_rect.width:
rl.draw_rectangle_gradient_ex(rl.Rectangle(text_field_rect.x, text_field_rect.y, 80, text_field_rect.height), rl.draw_rectangle_gradient_h(int(text_field_rect.x), int(text_field_rect.y), 80, int(text_field_rect.height),
rl.BLACK, rl.BLANK, rl.BLANK, rl.BLACK) rl.BLACK, rl.BLANK)
# draw cursor # draw cursor
blink_alpha = (math.sin(rl.get_time() * 6) + 1) / 2 blink_alpha = (math.sin(rl.get_time() * 6) + 1) / 2
@@ -162,14 +189,14 @@ class BigInputDialog(BigDialogBase):
cursor_x = min(text_x + text_size.x + 3, text_field_rect.x + text_field_rect.width) cursor_x = min(text_x + text_size.x + 3, text_field_rect.x + text_field_rect.width)
else: else:
cursor_x = text_field_rect.x - 6 cursor_x = text_field_rect.x - 6
rl.draw_rectangle_rounded(rl.Rectangle(cursor_x, text_field_rect.y, 4, text_size.y), rl.draw_rectangle_rounded(rl.Rectangle(int(cursor_x), int(text_field_rect.y), 4, int(text_size.y)),
1, 4, rl.Color(255, 255, 255, int(255 * blink_alpha))) 1, 4, rl.Color(255, 255, 255, int(255 * blink_alpha)))
# draw backspace icon with nice fade # draw backspace icon with nice fade
self._backspace_img_alpha.update(255 * bool(text)) self._backspace_img_alpha.update(255 * bool(text))
if self._backspace_img_alpha.x > 1: if self._backspace_img_alpha.x > 1:
color = rl.Color(255, 255, 255, int(self._backspace_img_alpha.x)) color = rl.Color(255, 255, 255, int(self._backspace_img_alpha.x))
rl.draw_texture_ex(self._backspace_img, rl.Vector2(self._rect.width - self._backspace_img.width - 27, self._rect.y + 14), 0.0, 1.0, color) rl.draw_texture(self._backspace_img, int(self._rect.width - self._backspace_img.width - 27), int(self._rect.y + 14), color)
if not text and self._hint_label.text and not candidate_char: if not text and self._hint_label.text and not candidate_char:
# draw description if no text entered yet and not drawing candidate char # draw description if no text entered yet and not drawing candidate char
@@ -187,9 +214,9 @@ class BigInputDialog(BigDialogBase):
# draw enter button # draw enter button
self._enter_img_alpha.update(255 if len(text) >= self._minimum_length else 0) self._enter_img_alpha.update(255 if len(text) >= self._minimum_length else 0)
color = rl.Color(255, 255, 255, int(self._enter_img_alpha.x)) color = rl.Color(255, 255, 255, int(self._enter_img_alpha.x))
rl.draw_texture_ex(self._enter_img, rl.Vector2(self._rect.x + PADDING / 2, self._rect.y), 0.0, 1.0, color) rl.draw_texture(self._enter_img, int(self._rect.x + PADDING / 2), int(self._rect.y), color)
color = rl.Color(255, 255, 255, 255 - int(self._enter_img_alpha.x)) color = rl.Color(255, 255, 255, 255 - int(self._enter_img_alpha.x))
rl.draw_texture_ex(self._enter_disabled_img, rl.Vector2(self._rect.x + PADDING / 2, self._rect.y), 0.0, 1.0, color) rl.draw_texture(self._enter_disabled_img, int(self._rect.x + PADDING / 2), int(self._rect.y), color)
# keyboard goes over everything # keyboard goes over everything
self._keyboard.render(self._rect) self._keyboard.render(self._rect)
@@ -226,15 +253,3 @@ class BigDialogButton(BigButton):
dlg = BigDialog(self.text, self._description) dlg = BigDialog(self.text, self._description)
gui_app.push_widget(dlg) gui_app.push_widget(dlg)
class BigConfirmationCircleButton(BigCircleButton):
def __init__(self, title: str, icon: rl.Texture, confirm_callback: Callable[[], None], exit_on_confirm: bool = True,
red: bool = False, icon_offset: tuple[int, int] = (0, 0)):
super().__init__(icon, red, icon_offset)
def show_confirm_dialog():
gui_app.push_widget(BigConfirmationDialog(title, icon, confirm_callback,
exit_on_confirm=exit_on_confirm, red=red))
self.set_click_callback(show_confirm_dialog)
+5 -4
View File
@@ -9,7 +9,7 @@ from openpilot.common.params import Params
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.widgets.nav_widget import NavWidget
from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.label import MiciLabel
class PairingDialog(NavWidget): class PairingDialog(NavWidget):
@@ -24,7 +24,8 @@ class PairingDialog(NavWidget):
self._last_qr_generation = float("-inf") self._last_qr_generation = float("-inf")
self._txt_pair = gui_app.texture("icons_mici/settings/device/pair.png", 33, 60) self._txt_pair = gui_app.texture("icons_mici/settings/device/pair.png", 33, 60)
self._pair_label = UnifiedLabel("pair with comma connect", font_size=48, font_weight=FontWeight.BOLD, line_height=0.8) self._pair_label = MiciLabel("pair with comma connect", 48, font_weight=FontWeight.BOLD,
color=rl.Color(255, 255, 255, int(255 * 0.9)), line_height=40, wrap_text=True)
def _get_pairing_url(self) -> str: def _get_pairing_url(self) -> str:
try: try:
@@ -76,7 +77,7 @@ class PairingDialog(NavWidget):
self._render_qr_code() self._render_qr_code()
label_x = self._rect.x + 8 + self._rect.height + 24 label_x = self._rect.x + 8 + self._rect.height + 24
self._pair_label.set_max_width(int(self._rect.width - label_x)) self._pair_label.set_width(int(self._rect.width - label_x))
self._pair_label.set_position(label_x, self._rect.y + 16) self._pair_label.set_position(label_x, self._rect.y + 16)
self._pair_label.render() self._pair_label.render()
@@ -92,7 +93,7 @@ class PairingDialog(NavWidget):
return return
scale = self._rect.height / self._qr_texture.height scale = self._rect.height / self._qr_texture.height
pos = rl.Vector2(round(self._rect.x + 8), round(self._rect.y)) pos = rl.Vector2(self._rect.x + 8, self._rect.y)
rl.draw_texture_ex(self._qr_texture, pos, 0.0, scale, rl.WHITE) rl.draw_texture_ex(self._qr_texture, pos, 0.0, scale, rl.WHITE)
def __del__(self): def __del__(self):
+1 -1
View File
@@ -118,7 +118,7 @@ class AlertRenderer(Widget):
alert = self.get_alert(ui_state.sm) alert = self.get_alert(ui_state.sm)
if gui_app.sunnypilot_ui(): if gui_app.sunnypilot_ui():
ui_state.onroad_brightness_handle_alerts(ui_state, alert) ui_state.onroad_brightness_handle_alerts(ui_state.started, alert)
if not alert: if not alert:
return return
+1 -1
View File
@@ -50,7 +50,7 @@ class ExpButton(Widget):
texture = self._txt_exp if self._held_or_actual_mode() else self._txt_wheel texture = self._txt_exp if self._held_or_actual_mode() else self._txt_wheel
rl.draw_circle(center_x, center_y, self._rect.width / 2, self._black_bg) rl.draw_circle(center_x, center_y, self._rect.width / 2, self._black_bg)
rl.draw_texture_ex(texture, rl.Vector2(center_x - texture.width / 2, center_y - texture.height / 2), 0.0, 1.0, self._white_color) rl.draw_texture(texture, center_x - texture.width // 2, center_y - texture.height // 2, self._white_color)
def _held_or_actual_mode(self): def _held_or_actual_mode(self):
now = time.monotonic() now = time.monotonic()
@@ -20,7 +20,7 @@ class SunnylinkConsentPage(Widget):
self._done_callback = done_callback self._done_callback = done_callback
self._step = 0 self._step = 0
self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)) self._title = Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._content = [ self._content = [
{ {
@@ -40,10 +40,9 @@ class SunnylinkConsentPage(Widget):
} }
] ]
self._primary_btn = self._child(Button("", button_style=ButtonStyle.PRIMARY, click_callback=lambda: self._handle_choice("enable"))) self._primary_btn = Button("", button_style=ButtonStyle.PRIMARY, click_callback=lambda: self._handle_choice("enable"))
self._secondary_btn = self._child(Button("", button_style=ButtonStyle.NORMAL, click_callback=lambda: self._handle_choice("secondary"))) self._secondary_btn = Button("", button_style=ButtonStyle.NORMAL, click_callback=lambda: self._handle_choice("secondary"))
self._danger_btn = self._child(Button("", button_style=ButtonStyle.DANGER, click_callback=lambda: self._handle_choice("disable"))) self._danger_btn = Button("", button_style=ButtonStyle.DANGER, click_callback=lambda: self._handle_choice("disable"))
self._desc = self._child(Label("", font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT))
def _handle_choice(self, choice): def _handle_choice(self, choice):
if choice == "enable": if choice == "enable":
@@ -74,8 +73,8 @@ class SunnylinkConsentPage(Widget):
desc_y = welcome_y + 120 desc_y = welcome_y + 120
desc_rect = rl.Rectangle(desc_x, desc_y, self._rect.width - desc_x, self._rect.height - desc_y - 250) desc_rect = rl.Rectangle(desc_x, desc_y, self._rect.width - desc_x, self._rect.height - desc_y - 250)
self._desc.set_text(step_data["text"]) desc_label = Label(step_data["text"], font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._desc.render(desc_rect) desc_label.render(desc_rect)
btn_y = self._rect.y + self._rect.height - 160 - 45 btn_y = self._rect.y + self._rect.height - 160 - 45
@@ -12,7 +12,8 @@ from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.sunnypilot.widgets.list_view import option_item_sp, ToggleActionSP from openpilot.system.ui.sunnypilot.widgets.list_view import option_item_sp, ToggleActionSP
from openpilot.sunnypilot.system.params_migration import ONROAD_BRIGHTNESS_TIMER_VALUES
ONROAD_BRIGHTNESS_TIMER_VALUES = {0: 15, 1: 30, **{i: (i - 1) * 60 for i in range(2, 12)}}
class OnroadBrightness(IntEnum): class OnroadBrightness(IntEnum):
@@ -45,7 +46,7 @@ class DisplayLayout(Widget):
title=lambda: tr("Onroad Brightness Delay"), title=lambda: tr("Onroad Brightness Delay"),
description="", description="",
min_value=0, min_value=0,
max_value=15, max_value=11,
value_change_step=1, value_change_step=1,
value_map=ONROAD_BRIGHTNESS_TIMER_VALUES, value_map=ONROAD_BRIGHTNESS_TIMER_VALUES,
label_callback=lambda value: f"{value} s" if value < 60 else f"{int(value/60)} m", label_callback=lambda value: f"{value} s" if value < 60 else f"{int(value/60)} m",
@@ -91,11 +92,7 @@ class DisplayLayout(Widget):
if isinstance(_item.action_item, ToggleActionSP) and _item.action_item.toggle.param_key is not None: 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)) _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: elif isinstance(_item.action_item, OptionControlSP) and _item.action_item.param_key is not None:
raw_value = self._params.get(_item.action_item.param_key, return_default=True) _item.action_item.set_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) 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)) self._onroad_brightness_timer.action_item.set_enabled(brightness_val not in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK))
@@ -82,7 +82,8 @@ class NavButton(Widget):
if self.panel_info.icon: if self.panel_info.icon:
icon_texture = gui_app.texture(self.panel_info.icon, ICON_SIZE, ICON_SIZE, keep_aspect_ratio=True) icon_texture = gui_app.texture(self.panel_info.icon, ICON_SIZE, ICON_SIZE, keep_aspect_ratio=True)
rl.draw_texture_ex(icon_texture, rl.Vector2(content_x, rect.y + (OP.NAV_BTN_HEIGHT - icon_texture.height) / 2), 0.0, 1.0, rl.WHITE) rl.draw_texture(icon_texture, int(content_x), int(rect.y + (OP.NAV_BTN_HEIGHT - icon_texture.height) / 2),
rl.WHITE)
content_x += ICON_SIZE + 20 content_x += ICON_SIZE + 20
# Draw button text (right-aligned) # Draw button text (right-aligned)

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