mirror of
https://github.com/dzid26/sunnypilot.git
synced 2026-06-08 07:44:55 +08:00
Merge branch 'upstream/openpilot/master' into sync-20260317
# Conflicts: # .github/workflows/auto_pr_review.yaml # .gitignore # opendbc_repo # panda # selfdrive/ui/mici/layouts/home.py # selfdrive/ui/mici/layouts/onboarding.py # selfdrive/ui/mici/layouts/settings/device.py # selfdrive/ui/tests/diff/replay.py # selfdrive/ui/translations/app_fr.po # system/ui/mici_setup.py Sync: `commaai/opendbc:master` → `sunnypilot/opendbc:master` Sync: `commaai/panda:master` → `sunnypilot/panda:master`
This commit is contained in:
1
.github/workflows/repo-maintenance.yaml
vendored
1
.github/workflows/repo-maintenance.yaml
vendored
@@ -72,7 +72,6 @@ jobs:
|
||||
git add .
|
||||
- name: update car docs
|
||||
run: |
|
||||
scons -j$(nproc) --minimal opendbc_repo
|
||||
python selfdrive/car/docs.py
|
||||
git add docs/CARS.md
|
||||
- name: Create Pull Request
|
||||
|
||||
2
.github/workflows/tests.yaml
vendored
2
.github/workflows/tests.yaml
vendored
@@ -181,7 +181,7 @@ jobs:
|
||||
echo "${{ github.sha }}" > ref_commit
|
||||
git add .
|
||||
git commit -m "process-replay refs for ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit"
|
||||
git push origin process-replay
|
||||
git push origin process-replay --force
|
||||
- name: Run regen
|
||||
if: false
|
||||
timeout-minutes: 4
|
||||
|
||||
37
.gitignore
vendored
37
.gitignore
vendored
@@ -13,13 +13,13 @@ venv/
|
||||
a.out
|
||||
.hypothesis
|
||||
.cache/
|
||||
|
||||
/docs_site/
|
||||
bin/
|
||||
|
||||
*.mp4
|
||||
*.dylib
|
||||
*.DSYM
|
||||
*.d
|
||||
*.pem
|
||||
*.pyc
|
||||
*.pyo
|
||||
.*.swp
|
||||
@@ -39,11 +39,13 @@ a.out
|
||||
*.mo
|
||||
*_pyx.cpp
|
||||
*.stats
|
||||
*.pkl
|
||||
*.pkl*
|
||||
config.json
|
||||
clcache
|
||||
compile_commands.json
|
||||
compare_runtime*.html
|
||||
|
||||
# build artifacts
|
||||
selfdrive/pandad/pandad
|
||||
cereal/services.h
|
||||
cereal/gen
|
||||
@@ -56,47 +58,30 @@ system/camerad/test/ae_gray_test
|
||||
.coverage*
|
||||
coverage.xml
|
||||
htmlcov
|
||||
pandaextra
|
||||
|
||||
.mypy_cache/
|
||||
flycheck_*
|
||||
|
||||
cppcheck_report.txt
|
||||
comma*.sh
|
||||
|
||||
selfdrive/modeld/models/*.pkl*
|
||||
sunnypilot/modeld*/models/*.pkl
|
||||
|
||||
# openpilot log files
|
||||
*.bz2
|
||||
*.zst
|
||||
*.rlog
|
||||
|
||||
build/
|
||||
|
||||
!**/.gitkeep
|
||||
|
||||
poetry.toml
|
||||
Pipfile
|
||||
|
||||
### VisualStudioCode ###
|
||||
*.vsix
|
||||
.history
|
||||
.ionide
|
||||
.vscode/*
|
||||
.history/
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
# agents
|
||||
.claude/
|
||||
.context/
|
||||
PLAN.md
|
||||
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.12.13
|
||||
8
Jenkinsfile
vendored
8
Jenkinsfile
vendored
@@ -167,7 +167,7 @@ node {
|
||||
env.GIT_COMMIT = checkout(scm).GIT_COMMIT
|
||||
|
||||
def excludeBranches = ['__nightly', 'devel', 'devel-staging', 'release3', 'release3-staging',
|
||||
'release-tici', 'release-tizi', 'release-tizi-staging', 'testing-closet*', 'hotfix-*']
|
||||
'release-tici', 'release-tizi', 'release-tizi-staging', 'release-mici-staging', 'testing-closet*', 'hotfix-*']
|
||||
def excludeRegex = excludeBranches.join('|').replaceAll('\\*', '.*')
|
||||
|
||||
if (env.BRANCH_NAME != 'master' && !env.BRANCH_NAME.contains('__jenkins_loop_')) {
|
||||
@@ -179,7 +179,7 @@ node {
|
||||
try {
|
||||
if (env.BRANCH_NAME == 'devel-staging') {
|
||||
deviceStage("build release-tizi-staging", "tizi-needs-can", [], [
|
||||
step("build release-tizi-staging", "RELEASE_BRANCH=release-tizi-staging $SOURCE_DIR/release/build_release.sh"),
|
||||
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"),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -218,14 +218,14 @@ node {
|
||||
'camerad OX03C10': {
|
||||
deviceStage("OX03C10", "tizi-ox03c10", ["UNSAFE=1"], [
|
||||
step("build", "cd system/manager && ./build.py"),
|
||||
step("test pandad", "pytest selfdrive/pandad/tests/test_pandad.py", [diffPaths: ["panda", "selfdrive/pandad/"]]),
|
||||
step("test pandad", "pytest selfdrive/pandad/tests/test_pandad.py"),
|
||||
step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 90]),
|
||||
])
|
||||
},
|
||||
'camerad OS04C10': {
|
||||
deviceStage("OS04C10", "tici-os04c10", ["UNSAFE=1"], [
|
||||
step("build", "cd system/manager && ./build.py"),
|
||||
step("test pandad", "pytest selfdrive/pandad/tests/test_pandad.py", [diffPaths: ["panda", "selfdrive/pandad/"]]),
|
||||
step("test pandad", "pytest selfdrive/pandad/tests/test_pandad.py"),
|
||||
step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 90]),
|
||||
])
|
||||
},
|
||||
|
||||
12
RELEASES.md
12
RELEASES.md
@@ -1,8 +1,16 @@
|
||||
Version 0.10.4 (2026-02-17)
|
||||
Version 0.11.1 (2026-04-08)
|
||||
========================
|
||||
* 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!
|
||||
* 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)
|
||||
========================
|
||||
|
||||
92
SConstruct
92
SConstruct
@@ -4,9 +4,11 @@ import sys
|
||||
import sysconfig
|
||||
import platform
|
||||
import shlex
|
||||
import importlib
|
||||
import numpy as np
|
||||
|
||||
import SCons.Errors
|
||||
from SCons.Defaults import _stripixes
|
||||
|
||||
SCons.Warnings.warningAsException(True)
|
||||
|
||||
@@ -14,9 +16,6 @@ Decider('MD5-timestamp')
|
||||
|
||||
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('--verbose', action='store_true', default=False, help='show full build commands')
|
||||
AddOption('--minimal',
|
||||
@@ -38,23 +37,46 @@ assert arch in [
|
||||
"Darwin", # macOS arm64 (x86 not supported)
|
||||
]
|
||||
|
||||
if arch != "larch64":
|
||||
import bzip2
|
||||
import capnproto
|
||||
import eigen
|
||||
import ffmpeg as ffmpeg_pkg
|
||||
import libjpeg
|
||||
import libyuv
|
||||
import ncurses
|
||||
import python3_dev
|
||||
import zeromq
|
||||
import zstd
|
||||
pkgs = [bzip2, capnproto, eigen, ffmpeg_pkg, libjpeg, libyuv, ncurses, zeromq, zstd]
|
||||
py_include = python3_dev.INCLUDE_DIR
|
||||
else:
|
||||
# TODO: remove when AGNOS has our new vendor pkgs
|
||||
pkgs = []
|
||||
py_include = sysconfig.get_paths()['include']
|
||||
pkg_names = ['bzip2', 'capnproto', 'eigen', 'ffmpeg', 'libjpeg', 'libyuv', 'ncurses', 'zeromq', 'zstd']
|
||||
pkgs = [importlib.import_module(name) for name in pkg_names]
|
||||
|
||||
|
||||
# ***** enforce a whitelist of system libraries *****
|
||||
# this prevents silently relying on a 3rd party package,
|
||||
# e.g. apt-installed libusb. all libraries should either
|
||||
# be distributed with all Linux distros and macOS, or
|
||||
# vendored in commaai/dependencies.
|
||||
allowed_system_libs = {
|
||||
"EGL", "GLESv2", "GL", "Qt5Charts", "Qt5Core", "Qt5Gui", "Qt5Widgets",
|
||||
"dl", "drm", "gbm", "m", "pthread",
|
||||
}
|
||||
|
||||
def _resolve_lib(env, name):
|
||||
for d in env.Flatten(env.get('LIBPATH', [])):
|
||||
p = Dir(str(d)).abspath
|
||||
for ext in ('.a', '.so', '.dylib'):
|
||||
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={
|
||||
@@ -107,14 +129,14 @@ env = Environment(
|
||||
tools=["default", "cython", "compilation_db", "rednose_filter"],
|
||||
toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"],
|
||||
)
|
||||
if arch != "larch64":
|
||||
env['_LIBFLAGS'] = _libflags
|
||||
|
||||
# Arch-specific flags and paths
|
||||
if arch == "larch64":
|
||||
env["CC"] = "clang"
|
||||
env["CXX"] = "clang++"
|
||||
env.Append(LIBPATH=[
|
||||
"/usr/local/lib",
|
||||
"/system/vendor/lib64",
|
||||
"/usr/lib/aarch64-linux-gnu",
|
||||
])
|
||||
arch_flags = ["-D__TICI__", "-mcpu=cortex-a57", "-DQCOM2"]
|
||||
@@ -126,19 +148,6 @@ elif arch == "Darwin":
|
||||
])
|
||||
env.Append(CCFLAGS=["-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 '')
|
||||
if _extra_cc:
|
||||
@@ -176,7 +185,7 @@ if os.environ.get('SCONS_PROGRESS'):
|
||||
|
||||
# ********** Cython build environment **********
|
||||
envCython = env.Clone()
|
||||
envCython["CPPPATH"] += [py_include, np.get_include()]
|
||||
envCython["CPPPATH"] += [sysconfig.get_paths()['include'], np.get_include()]
|
||||
envCython["CCFLAGS"] += ["-Wno-#warnings", "-Wno-cpp", "-Wno-shadow", "-Wno-deprecated-declarations"]
|
||||
envCython["CCFLAGS"].remove("-Werror")
|
||||
|
||||
@@ -210,7 +219,6 @@ Export('common')
|
||||
env_swaglog = env.Clone()
|
||||
env_swaglog['CXXFLAGS'].append('-DSWAGLOG="\\"common/swaglog.h\\""')
|
||||
SConscript(['msgq_repo/SConscript'], exports={'env': env_swaglog})
|
||||
SConscript(['opendbc_repo/SConscript'], exports={'env': env_swaglog})
|
||||
|
||||
SConscript(['cereal/SConscript'])
|
||||
|
||||
@@ -236,7 +244,15 @@ if arch == "larch64":
|
||||
# Build openpilot
|
||||
SConscript(['third_party/SConscript'])
|
||||
|
||||
SConscript(['selfdrive/SConscript'])
|
||||
# Build selfdrive
|
||||
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'])
|
||||
|
||||
|
||||
1
common/.gitignore
vendored
1
common/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
*.cpp
|
||||
@@ -1,4 +1,4 @@
|
||||
Import('env', 'envCython', 'arch')
|
||||
Import('env', 'envCython')
|
||||
|
||||
common_libs = [
|
||||
'params.cc',
|
||||
|
||||
@@ -28,7 +28,7 @@ class BounceFilter(FirstOrderFilter):
|
||||
scale = self.dt / (1.0 / 60.0) # tuned at 60 fps
|
||||
self.velocity.x += (x - self.x) * self.bounce * scale * self.dt
|
||||
self.velocity.update(0.0)
|
||||
if abs(self.velocity.x) < 1e-5:
|
||||
if abs(self.velocity.x) < 1e-3:
|
||||
self.velocity.x = 0.0
|
||||
self.x += self.velocity.x
|
||||
return self.x
|
||||
|
||||
@@ -2,6 +2,7 @@ import datetime
|
||||
from pathlib import Path
|
||||
|
||||
MIN_DATE = datetime.datetime(year=2025, month=2, day=21)
|
||||
MAX_DATE = datetime.datetime(year=2035, month=1, day=1)
|
||||
|
||||
def min_date():
|
||||
# on systemd systems, the default time is the systemd build time
|
||||
@@ -12,4 +13,4 @@ def min_date():
|
||||
return MIN_DATE
|
||||
|
||||
def system_time_valid():
|
||||
return datetime.datetime.now() > min_date()
|
||||
return min_date() < datetime.datetime.now() < MAX_DATE
|
||||
|
||||
2
common/transformations/.gitignore
vendored
2
common/transformations/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
transformations
|
||||
transformations.cpp
|
||||
@@ -1 +1 @@
|
||||
#define COMMA_VERSION "0.10.4"
|
||||
#define COMMA_VERSION "0.11.1"
|
||||
|
||||
@@ -10,7 +10,6 @@ from openpilot.system.hardware import TICI, HARDWARE
|
||||
# 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
|
||||
collect_ignore = [
|
||||
"selfdrive/ui/tests/test_translations",
|
||||
"selfdrive/test/process_replay/test_processes.py",
|
||||
"selfdrive/test/process_replay/test_regen.py",
|
||||
]
|
||||
|
||||
@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
|
||||
export QCOM_PRIORITY=12
|
||||
|
||||
if [ -z "$AGNOS_VERSION" ]; then
|
||||
export AGNOS_VERSION="16"
|
||||
export AGNOS_VERSION="17.2"
|
||||
fi
|
||||
|
||||
export STAGING_ROOT="/data/safe_staging"
|
||||
|
||||
Submodule opendbc_repo updated: 96a96b80da...b178bc5d4e
2
panda
2
panda
Submodule panda updated: f5f296c65c...6ddc631bdd
@@ -26,18 +26,18 @@ dependencies = [
|
||||
"numpy >=2.0",
|
||||
|
||||
# vendored native dependencies
|
||||
"bzip2 @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=bzip2",
|
||||
"capnproto @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=capnproto",
|
||||
"eigen @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=eigen",
|
||||
"ffmpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ffmpeg",
|
||||
"libjpeg @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=libjpeg",
|
||||
"libyuv @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=libyuv",
|
||||
"python3-dev @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=python3-dev",
|
||||
"zstd @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zstd",
|
||||
"ncurses @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ncurses",
|
||||
"zeromq @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zeromq",
|
||||
"git-lfs @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=git-lfs",
|
||||
"gcc-arm-none-eabi @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=gcc-arm-none-eabi",
|
||||
"bzip2 @ git+https://github.com/commaai/dependencies.git@release-bzip2#subdirectory=bzip2",
|
||||
"capnproto @ git+https://github.com/commaai/dependencies.git@release-capnproto#subdirectory=capnproto",
|
||||
"eigen @ git+https://github.com/commaai/dependencies.git@release-eigen#subdirectory=eigen",
|
||||
"ffmpeg @ git+https://github.com/commaai/dependencies.git@release-ffmpeg#subdirectory=ffmpeg",
|
||||
"libjpeg @ git+https://github.com/commaai/dependencies.git@release-libjpeg#subdirectory=libjpeg",
|
||||
"libyuv @ git+https://github.com/commaai/dependencies.git@release-libyuv#subdirectory=libyuv",
|
||||
"zstd @ git+https://github.com/commaai/dependencies.git@release-zstd#subdirectory=zstd",
|
||||
"ncurses @ git+https://github.com/commaai/dependencies.git@release-ncurses#subdirectory=ncurses",
|
||||
"zeromq @ git+https://github.com/commaai/dependencies.git@release-zeromq#subdirectory=zeromq",
|
||||
"libusb @ git+https://github.com/commaai/dependencies.git@release-libusb#subdirectory=libusb",
|
||||
"git-lfs @ git+https://github.com/commaai/dependencies.git@release-git-lfs#subdirectory=git-lfs",
|
||||
"gcc-arm-none-eabi @ git+https://github.com/commaai/dependencies.git@release-gcc-arm-none-eabi#subdirectory=gcc-arm-none-eabi",
|
||||
|
||||
# body / webrtcd
|
||||
"av",
|
||||
@@ -206,6 +206,7 @@ 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_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_texture".msg = "Use rl.draw_texture_ex for float position support"
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "preserve"
|
||||
@@ -249,3 +250,6 @@ unsupported-operator = "ignore"
|
||||
# Ignore not-subscriptable - false positives from dynamic types
|
||||
not-subscriptable = "ignore"
|
||||
# not-iterable errors are now fixed
|
||||
|
||||
[tool.uv]
|
||||
python-preference = "only-managed"
|
||||
|
||||
@@ -13,11 +13,12 @@ from openpilot.common.basedir import BASEDIR
|
||||
|
||||
DIRS = ['cereal', 'openpilot']
|
||||
EXTS = ['.png', '.py', '.ttf', '.capnp', '.json', '.fnt', '.mo', '.po']
|
||||
EXCLUDE = ['selfdrive/assets/training', 'third_party/raylib/raylib_repo/examples']
|
||||
INTERPRETER = '/usr/bin/env python3'
|
||||
|
||||
|
||||
def copy(src, dest):
|
||||
if any(src.endswith(ext) for ext in EXTS):
|
||||
if any(src.endswith(ext) for ext in EXTS) and not any(exc in src for exc in EXCLUDE):
|
||||
shutil.copy2(src, dest, follow_symlinks=True)
|
||||
|
||||
|
||||
@@ -28,6 +29,8 @@ if __name__ == '__main__':
|
||||
parser.add_argument('module', help="the module to target, e.g. 'openpilot.system.ui.spinner'")
|
||||
args = parser.parse_args()
|
||||
|
||||
print('WARNING: copying all files! make sure to run scons and git tree is clean')
|
||||
|
||||
if not args.output:
|
||||
args.output = args.module
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
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
selfdrive/assets/.gitignore
vendored
2
selfdrive/assets/.gitignore
vendored
@@ -1,4 +1,2 @@
|
||||
*.cc
|
||||
fonts/*.fnt
|
||||
fonts/*.png
|
||||
translations_assets.qrc
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
selfdrive/assets/icons_mici/setup/factory_reset.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/setup/factory_reset.png
LFS
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
selfdrive/assets/icons_mici/setup/reset_failed.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/setup/reset_failed.png
LFS
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
selfdrive/car/tests/.gitignore
vendored
1
selfdrive/car/tests/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
*.bz2
|
||||
2
selfdrive/controls/.gitignore
vendored
2
selfdrive/controls/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
calibration_param
|
||||
traces
|
||||
2
selfdrive/locationd/.gitignore
vendored
2
selfdrive/locationd/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
params_learner
|
||||
paramsd
|
||||
@@ -29,11 +29,26 @@ MIN_LAG = 0.15
|
||||
MAX_LAG_STD = 0.1
|
||||
MAX_LAT_ACCEL = 2.0
|
||||
MAX_LAT_ACCEL_DIFF = 0.6
|
||||
MIN_LAT_ACCEL_RANGE = 0.5
|
||||
MIN_CONFIDENCE = 0.7
|
||||
CORR_BORDER_OFFSET = 5
|
||||
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):
|
||||
"""
|
||||
References:
|
||||
@@ -295,11 +310,14 @@ class LateralLagEstimator:
|
||||
|
||||
times, desired, actual, okay = self.points.get()
|
||||
# check if there are any new valid data points since the last update
|
||||
is_valid = self.points_valid()
|
||||
is_valid = self.points_valid() and (actual.max() - actual.min() >= MIN_LAT_ACCEL_RANGE)
|
||||
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)
|
||||
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)
|
||||
if corr < self.min_ncc or confidence < self.min_confidence or not is_valid:
|
||||
return
|
||||
@@ -311,16 +329,16 @@ class LateralLagEstimator:
|
||||
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]:
|
||||
assert len(expected_sig) == len(actual_sig)
|
||||
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_lag_samples)
|
||||
min_lag_samples, max_lag_samples, one_sec_samples = int(round(min_lag / dt)), int(round(max_lag / dt)), int(round(1.0 / dt))
|
||||
padded_size = fft_next_good_size(len(expected_sig) + max(max_lag_samples, one_sec_samples))
|
||||
|
||||
ncc = masked_normalized_cross_correlation(expected_sig, actual_sig, mask, padded_size)
|
||||
|
||||
# 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]
|
||||
extended_roi = np.s_[roi.start - CORR_BORDER_OFFSET: roi.stop + CORR_BORDER_OFFSET]
|
||||
roi_ncc = ncc[roi]
|
||||
extended_roi_ncc = ncc[extended_roi]
|
||||
# only consider lags from ranges:
|
||||
roi = np.s_[len(expected_sig) - 1 + min_lag_samples: len(expected_sig) - 1 + max_lag_samples] # min_lag - max_lag range
|
||||
threshold_roi = np.s_[len(expected_sig) - 1: len(expected_sig) - 1 + one_sec_samples] # 0 - 1 second range
|
||||
confidence_roi = np.s_[threshold_roi.start - CORR_BORDER_OFFSET: threshold_roi.stop + CORR_BORDER_OFFSET] # threshold range +/- border
|
||||
roi_ncc, confidence_roi_ncc, threshold_roi_ncc = ncc[roi], ncc[confidence_roi], ncc[threshold_roi]
|
||||
|
||||
max_corr_index = np.argmax(roi_ncc)
|
||||
corr = roi_ncc[max_corr_index]
|
||||
@@ -328,8 +346,8 @@ class LateralLagEstimator:
|
||||
|
||||
# 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
|
||||
ncc_thresh = (roi_ncc.max() - roi_ncc.min()) * LAG_CANDIDATE_CORR_THRESHOLD + roi_ncc.min()
|
||||
good_lag_candidate_mask = extended_roi_ncc >= ncc_thresh
|
||||
ncc_thresh = (threshold_roi_ncc.max() - threshold_roi_ncc.min()) * LAG_CANDIDATE_CORR_THRESHOLD + threshold_roi_ncc.min()
|
||||
good_lag_candidate_mask = confidence_roi_ncc >= ncc_thresh
|
||||
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
|
||||
run_idx = np.searchsorted(starts, max_corr_index + CORR_BORDER_OFFSET, side='right') - 1
|
||||
|
||||
1
selfdrive/locationd/test/.gitignore
vendored
1
selfdrive/locationd/test/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
out/
|
||||
@@ -19,8 +19,8 @@ DT = 0.05
|
||||
def process_messages(estimator, lag_frames, n_frames, vego=20.0, rejection_threshold=0.0):
|
||||
for i in range(n_frames):
|
||||
t = i * estimator.dt
|
||||
desired_la = np.cos(10 * t) * 0.1
|
||||
actual_la = np.cos(10 * (t - lag_frames * estimator.dt)) * 0.1
|
||||
desired_la = np.cos(10 * t) * 0.3
|
||||
actual_la = np.cos(10 * (t - lag_frames * estimator.dt)) * 0.3
|
||||
|
||||
# if sample is masked out, set it to desired value (no lag)
|
||||
rejected = random.uniform(0, 1) < rejection_threshold
|
||||
|
||||
@@ -45,13 +45,17 @@ def tg_compile(flags, model_name):
|
||||
pkl = fn + "_tinygrad.pkl"
|
||||
onnx_path = fn + ".onnx"
|
||||
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):
|
||||
chunk_file(pkl, chunk_targets)
|
||||
return lenv.Command(
|
||||
chunk_targets,
|
||||
[onnx_path] + tinygrad_files + [chunker_file],
|
||||
[f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {pkl}',
|
||||
do_chunk]
|
||||
compile_node,
|
||||
do_chunk,
|
||||
)
|
||||
|
||||
# Compile small models
|
||||
|
||||
2
selfdrive/test/.gitignore
vendored
2
selfdrive/test/.gitignore
vendored
@@ -3,7 +3,7 @@ docker_out/
|
||||
|
||||
process_replay/diff.txt
|
||||
process_replay/model_diff.txt
|
||||
process_replay/fakedata/
|
||||
valgrind_logs.txt
|
||||
|
||||
*.bz2
|
||||
*.hevc
|
||||
|
||||
1
selfdrive/test/process_replay/.gitignore
vendored
1
selfdrive/test/process_replay/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
fakedata/
|
||||
@@ -342,10 +342,15 @@ class TestOnroad:
|
||||
|
||||
start, end = min(first_fid), min(last_fid)
|
||||
for i in range(end-start):
|
||||
ts = {c: round(self.ts[c]['timestampSof'][i]/1e6, 1) for c in cams}
|
||||
# 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[:2]}
|
||||
diff = (max(ts.values()) - min(ts.values()))
|
||||
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):
|
||||
# sanity check that the frame metadata is consistent with the encoded frames
|
||||
pairs = [('roadCameraState', 'roadEncodeIdx'),
|
||||
|
||||
3
selfdrive/ui/.gitignore
vendored
3
selfdrive/ui/.gitignore
vendored
@@ -1 +1,4 @@
|
||||
installer/installers/*
|
||||
|
||||
tests/diff/report
|
||||
.coverage
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
Import('env', 'arch', 'common')
|
||||
|
||||
@@ -19,7 +18,6 @@ env.Command(
|
||||
|
||||
if GetOption('extras') and arch == "larch64":
|
||||
# build installers
|
||||
if arch != "Darwin":
|
||||
raylib_env = env.Clone()
|
||||
raylib_env['LIBPATH'] += [f'#third_party/raylib/{arch}/']
|
||||
raylib_env['LINKFLAGS'].append('-Wl,-strip-debug')
|
||||
|
||||
@@ -81,14 +81,16 @@ void run(const char* cmd) {
|
||||
}
|
||||
|
||||
void finishInstall() {
|
||||
if (tici_device) {
|
||||
BeginDrawing();
|
||||
ClearBackground(BLACK);
|
||||
if (tici_device) {
|
||||
const char *m = "Finishing install...";
|
||||
int text_width = MeasureText(m, FONT_SIZE);
|
||||
DrawTextEx(font_display, m, (Vector2){(float)(GetScreenWidth() - text_width)/2 + FONT_SIZE, (float)(GetScreenHeight() - FONT_SIZE)/2}, FONT_SIZE, 0, WHITE);
|
||||
EndDrawing();
|
||||
} else {
|
||||
DrawTextEx(font_display, "finishing setup", (Vector2){12, 0}, 77, 0, (Color){255, 255, 255, (unsigned char)(255 * 0.9)});
|
||||
}
|
||||
EndDrawing();
|
||||
util::sleep_for(60 * 1000);
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ class HomeLayout(Widget):
|
||||
self._setup_callbacks()
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._exp_mode_button.show_event()
|
||||
self.last_refresh = time.monotonic()
|
||||
self._refresh()
|
||||
|
||||
@@ -94,7 +94,7 @@ class TrainingGuide(Widget):
|
||||
def _render(self, _):
|
||||
# Safeguard against fast tapping
|
||||
step = min(self._step, len(self._textures) - 1)
|
||||
rl.draw_texture(self._textures[step], 0, 0, rl.WHITE)
|
||||
rl.draw_texture_ex(self._textures[step], rl.Vector2(0, 0), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
# progress bar
|
||||
if 0 < step < len(STEP_RECTS) - 1:
|
||||
|
||||
@@ -104,6 +104,7 @@ class DeveloperLayout(Widget):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._scroller.show_event()
|
||||
self._update_toggles()
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ class DeviceLayout(Widget):
|
||||
self._power_off_btn.action_item.right_button.set_visible(ui_state.is_offroad())
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._scroller.show_event()
|
||||
|
||||
def _render(self, rect):
|
||||
|
||||
@@ -82,6 +82,7 @@ class SoftwareLayout(Widget):
|
||||
], line_separator=True, spacing=0)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._scroller.show_event()
|
||||
|
||||
def _render(self, rect):
|
||||
|
||||
@@ -152,6 +152,7 @@ class TogglesLayout(Widget):
|
||||
ui_state.personality = personality
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._scroller.show_event()
|
||||
self._update_toggles()
|
||||
|
||||
|
||||
@@ -165,14 +165,14 @@ class Sidebar(Widget, SidebarSP):
|
||||
# Settings button
|
||||
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
|
||||
rl.draw_texture(self._settings_img, int(SETTINGS_BTN.x), int(SETTINGS_BTN.y), tint)
|
||||
rl.draw_texture_ex(self._settings_img, rl.Vector2(SETTINGS_BTN.x, SETTINGS_BTN.y), 0.0, 1.0, tint)
|
||||
|
||||
# Home/Flag button
|
||||
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
|
||||
|
||||
tint = Colors.BUTTON_PRESSED if (ui_state.started and flag_pressed) else Colors.BUTTON_NORMAL
|
||||
rl.draw_texture(button_img, int(HOME_BTN.x), int(HOME_BTN.y), tint)
|
||||
rl.draw_texture_ex(button_img, rl.Vector2(HOME_BTN.x, HOME_BTN.y), 0.0, 1.0, tint)
|
||||
|
||||
# Microphone button
|
||||
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
|
||||
|
||||
rl.draw_rectangle_rounded(self._mic_indicator_rect, 1, 10, bg_color)
|
||||
rl.draw_texture(self._mic_img, int(self._mic_indicator_rect.x + (self._mic_indicator_rect.width - self._mic_img.width) / 2),
|
||||
int(self._mic_indicator_rect.y + (self._mic_indicator_rect.height - self._mic_img.height) / 2), Colors.WHITE)
|
||||
rl.draw_texture_ex(self._mic_img, rl.Vector2(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)
|
||||
|
||||
def _draw_network_indicator(self, rect: rl.Rectangle):
|
||||
# Signal strength dots
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections.abc import Callable
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.layouts import HBoxLayout
|
||||
from openpilot.system.ui.widgets.icon_widget import IconWidget
|
||||
from openpilot.system.ui.widgets.label import MiciLabel, UnifiedLabel
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
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
|
||||
draw_y -= (self._wifi_slash_txt.height - self._wifi_none_txt.height) / 2
|
||||
|
||||
rl.draw_texture(draw_net_txt, int(draw_x), int(draw_y), rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
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)))
|
||||
|
||||
|
||||
class MiciHomeLayout(Widget):
|
||||
@@ -103,14 +103,15 @@ class MiciHomeLayout(Widget):
|
||||
self._mic_icon,
|
||||
], spacing=18)
|
||||
|
||||
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 = MiciLabel("", font_size=36, font_weight=FontWeight.ROMAN)
|
||||
self._large_version_label = MiciLabel("", font_size=64, color=rl.GRAY, font_weight=FontWeight.ROMAN)
|
||||
self._date_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
|
||||
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False)
|
||||
self._version_label = UnifiedLabel("", font_size=36, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||
self._large_version_label = UnifiedLabel("", font_size=64, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||
self._date_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||
self._branch_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, scroll=True)
|
||||
self._version_commit_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
|
||||
self._version_commit_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._version_text = self._get_version_text()
|
||||
self._update_params()
|
||||
|
||||
@@ -182,12 +183,12 @@ class MiciHomeLayout(Widget):
|
||||
self._version_label.render()
|
||||
|
||||
self._date_label.set_text(" " + self._version_text[3])
|
||||
self._date_label.set_position(version_pos.x + self._version_label.rect.width + 10, version_pos.y)
|
||||
self._date_label.set_position(version_pos.x + self._version_label.text_width + 10, version_pos.y)
|
||||
self._date_label.render()
|
||||
|
||||
self._branch_label.set_max_width(gui_app.width - self._version_label.rect.width - self._date_label.rect.width - 32)
|
||||
self._branch_label.set_max_width(gui_app.width - self._version_label.text_width - self._date_label.text_width - 32)
|
||||
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.rect.width + self._date_label.rect.width + 20, version_pos.y)
|
||||
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.render()
|
||||
|
||||
if not release_branch:
|
||||
|
||||
@@ -144,7 +144,7 @@ class AlertItem(Widget):
|
||||
bg_texture = self._bg_small_pressed if self.is_pressed else self._bg_small
|
||||
|
||||
# Draw background
|
||||
rl.draw_texture(bg_texture, int(self._rect.x), int(self._rect.y), rl.WHITE)
|
||||
rl.draw_texture_ex(bg_texture, rl.Vector2(self._rect.x, self._rect.y), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
# Calculate text area (left side, avoiding icon on right)
|
||||
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_x = self._rect.x + self.ALERT_WIDTH - self.ALERT_PADDING - self.ICON_SIZE
|
||||
icon_y = self._rect.y + self.ALERT_PADDING
|
||||
rl.draw_texture(icon_texture, int(icon_x), int(icon_y), rl.WHITE)
|
||||
rl.draw_texture_ex(icon_texture, rl.Vector2(icon_x, icon_y), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
|
||||
class MiciOffroadAlerts(Scroller):
|
||||
|
||||
@@ -15,8 +15,7 @@ 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 sunnylink_consent_version, sunnylink_consent_declined
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialogV2
|
||||
from openpilot.selfdrive.ui.mici.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
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkConsentPage
|
||||
@@ -154,8 +153,10 @@ class TrainingGuideDMTutorial(NavWidget):
|
||||
def _render(self, _):
|
||||
self._dialog.render(self._rect)
|
||||
|
||||
rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - 80),
|
||||
int(self._rect.width), 80, rl.BLANK, rl.BLACK)
|
||||
gradient_y = int(self._rect.y + self._rect.height - 80)
|
||||
gradient_h = int(self._rect.y) + int(self._rect.height) - gradient_y
|
||||
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
|
||||
ring_thickness = 8
|
||||
@@ -217,26 +218,19 @@ class TrainingGuideRecordFront(NavScroller):
|
||||
def __init__(self, continue_callback: Callable[[], None]):
|
||||
super().__init__()
|
||||
|
||||
def show_accept_dialog():
|
||||
def on_accept():
|
||||
ui_state.params.put_bool_nonblocking("RecordFront", True)
|
||||
continue_callback()
|
||||
|
||||
gui_app.push_widget(BigConfirmationDialogV2("allow data uploading", "icons_mici/setup/driver_monitoring/dm_check.png", exit_on_confirm=False,
|
||||
confirm_callback=on_accept))
|
||||
|
||||
def show_decline_dialog():
|
||||
def on_decline():
|
||||
ui_state.params.put_bool_nonblocking("RecordFront", False)
|
||||
continue_callback()
|
||||
|
||||
gui_app.push_widget(BigConfirmationDialogV2("no, don't upload", "icons_mici/setup/cancel.png", exit_on_confirm=False, confirm_callback=on_decline))
|
||||
self._accept_button = BigConfirmationCircleButton("allow data uploading", gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 64, 64),
|
||||
on_accept, exit_on_confirm=False)
|
||||
|
||||
self._accept_button = BigCircleButton("icons_mici/setup/driver_monitoring/dm_check.png")
|
||||
self._accept_button.set_click_callback(show_accept_dialog)
|
||||
|
||||
self._decline_button = BigCircleButton("icons_mici/setup/cancel.png")
|
||||
self._decline_button.set_click_callback(show_decline_dialog)
|
||||
self._decline_button = BigConfirmationCircleButton("no, don't upload", gui_app.texture("icons_mici/setup/cancel.png", 64, 64), on_decline,
|
||||
exit_on_confirm=False)
|
||||
|
||||
self._scroller.add_widgets([
|
||||
GreyBigButton("driver camera data", "do you want to share video data for training?",
|
||||
@@ -276,12 +270,9 @@ class TrainingGuide(NavWidget):
|
||||
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 show_event(self):
|
||||
super().show_event()
|
||||
self._steps[0].show_event()
|
||||
|
||||
def _render(self, _):
|
||||
self._steps[0].render(self._rect)
|
||||
|
||||
@@ -314,7 +305,7 @@ class QRCodeWidget(Widget):
|
||||
def _render(self, _):
|
||||
if self._qr_texture:
|
||||
scale = self._size / self._qr_texture.height
|
||||
rl.draw_texture_ex(self._qr_texture, rl.Vector2(self._rect.x, self._rect.y), 0.0, scale, rl.WHITE)
|
||||
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:
|
||||
@@ -325,27 +316,20 @@ class TermsPage(Scroller):
|
||||
def __init__(self, on_accept, on_decline):
|
||||
super().__init__()
|
||||
|
||||
def show_accept_dialog():
|
||||
gui_app.push_widget(BigConfirmationDialogV2("accept\nterms", "icons_mici/setup/driver_monitoring/dm_check.png",
|
||||
confirm_callback=on_accept))
|
||||
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)
|
||||
|
||||
def show_decline_dialog():
|
||||
gui_app.push_widget(BigConfirmationDialogV2("decline &\nuninstall", "icons_mici/setup/cancel.png",
|
||||
red=True, exit_on_confirm=False, confirm_callback=on_decline))
|
||||
|
||||
self._accept_button = BigCircleButton("icons_mici/setup/driver_monitoring/dm_check.png")
|
||||
self._accept_button.set_click_callback(show_accept_dialog)
|
||||
|
||||
self._decline_button = BigCircleButton("icons_mici/setup/cancel.png", red=True)
|
||||
self._decline_button.set_click_callback(show_decline_dialog)
|
||||
self._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([
|
||||
GreyBigButton("terms and\nconditions", "scroll to continue",
|
||||
gui_app.texture("icons_mici/setup/green_info.png", 64, 64)),
|
||||
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"),
|
||||
GreyBigButton("", "You must accept the Terms & Conditions to use sunnypilot."),
|
||||
self._must_accept_card,
|
||||
self._accept_button,
|
||||
self._decline_button,
|
||||
])
|
||||
|
||||
@@ -5,32 +5,37 @@ from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigInputDialog
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.widgets.ssh_key import SshKeyAction
|
||||
from openpilot.selfdrive.ui.widgets.ssh_key import SshKeyFetcher
|
||||
|
||||
|
||||
class DeveloperLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._ssh_fetcher = SshKeyFetcher(ui_state.params)
|
||||
|
||||
def github_username_callback(username: str):
|
||||
if username:
|
||||
ssh_keys = SshKeyAction()
|
||||
ssh_keys._fetch_ssh_key(username)
|
||||
if not ssh_keys._error_message:
|
||||
self._ssh_keys_btn.set_value("Loading...")
|
||||
self._ssh_keys_btn.set_enabled(False)
|
||||
|
||||
def on_response(error):
|
||||
self._ssh_keys_btn.set_enabled(True)
|
||||
if error is None:
|
||||
self._ssh_keys_btn.set_value(username)
|
||||
else:
|
||||
dlg = BigDialog("", ssh_keys._error_message)
|
||||
gui_app.push_widget(dlg)
|
||||
self._ssh_keys_btn.set_value("Not set")
|
||||
gui_app.push_widget(BigDialog("", error))
|
||||
|
||||
self._ssh_fetcher.fetch(username, on_response)
|
||||
else:
|
||||
ui_state.params.remove("GithubUsername")
|
||||
ui_state.params.remove("GithubSshKeys")
|
||||
self._ssh_fetcher.clear()
|
||||
self._ssh_keys_btn.set_value("Not set")
|
||||
|
||||
def ssh_keys_callback():
|
||||
github_username = ui_state.params.get("GithubUsername") or ""
|
||||
dlg = BigInputDialog("enter GitHub username...", github_username, minimum_length=0, confirm_callback=github_username_callback)
|
||||
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)
|
||||
return
|
||||
gui_app.push_widget(dlg)
|
||||
@@ -42,8 +47,8 @@ class DeveloperLayoutMici(NavScroller):
|
||||
|
||||
# adb, ssh, ssh keys, debug mode, joystick debug mode, longitudinal maneuver mode, ip address
|
||||
# ******** Main Scroller ********
|
||||
self._adb_toggle = BigCircleParamControl("icons_mici/adb_short.png", "AdbEnabled", icon_size=(82, 82), icon_offset=(0, 12))
|
||||
self._ssh_toggle = BigCircleParamControl("icons_mici/ssh_short.png", "SshEnabled", icon_size=(82, 82), icon_offset=(0, 12))
|
||||
self._adb_toggle = BigCircleParamControl(gui_app.texture("icons_mici/adb_short.png", 82, 82), "AdbEnabled", icon_offset=(0, 12))
|
||||
self._ssh_toggle = BigCircleParamControl(gui_app.texture("icons_mici/ssh_short.png", 82, 82), "SshEnabled", icon_offset=(0, 12))
|
||||
self._joystick_toggle = BigToggle("joystick debug mode",
|
||||
initial_state=ui_state.params.get_bool("JoystickDebugMode"),
|
||||
toggle_callback=self._on_joystick_debug_mode)
|
||||
@@ -99,6 +104,10 @@ class DeveloperLayoutMici(NavScroller):
|
||||
|
||||
ui_state.add_offroad_transition_callback(self._update_toggles)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
self._ssh_fetcher.update()
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._update_toggles()
|
||||
|
||||
@@ -9,16 +9,15 @@ from openpilot.common.params import Params
|
||||
from openpilot.common.time_helpers import system_time_valid
|
||||
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.dialog import BigDialog, BigConfirmationDialogV2
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog
|
||||
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage
|
||||
from openpilot.system.ui.mici_setup import BigPillButton
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
||||
from openpilot.system.ui.widgets.label import MiciLabel
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.html_render import HtmlModal, HtmlRenderer
|
||||
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
|
||||
|
||||
@@ -27,13 +26,11 @@ 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)
|
||||
|
||||
close_button = BigPillButton("close")
|
||||
close_button.set_click_callback(self.dismiss)
|
||||
self._scroller.add_widget(close_button)
|
||||
|
||||
|
||||
class ReviewTrainingGuide(TrainingGuide):
|
||||
def show_event(self):
|
||||
@@ -67,34 +64,31 @@ class MiciFccModal(NavRawScrollPanel):
|
||||
rl.draw_texture_ex(self._fcc_logo, fcc_pos, 0.0, 1.0, rl.WHITE)
|
||||
|
||||
|
||||
def _engaged_confirmation_callback(callback: Callable, action_text: str):
|
||||
def _engaged_confirmation_click(callback: Callable, action_text: str, icon: rl.Texture, exit_on_confirm: bool = True, red: bool = False):
|
||||
if not ui_state.engaged:
|
||||
def confirm_callback():
|
||||
# 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:
|
||||
callback()
|
||||
|
||||
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"
|
||||
gui_app.push_widget(BigConfirmationDialog(f"slide to\n{action_text.lower()}", icon, confirm_callback, exit_on_confirm=exit_on_confirm, red=red))
|
||||
else:
|
||||
# TODO: check
|
||||
icon = "icons_mici/settings/comma_icon.png"
|
||||
gui_app.push_widget(BigDialog("", f"Disengage to {action_text}"))
|
||||
|
||||
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:
|
||||
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):
|
||||
@@ -104,14 +98,15 @@ class DeviceInfoLayoutMici(Widget):
|
||||
self.set_rect(rl.Rectangle(0, 0, 360, 180))
|
||||
|
||||
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))
|
||||
max_width = int(self._rect.width - 20)
|
||||
self._dongle_id_label = MiciLabel("device ID", 48, width=max_width, color=header_color, font_weight=FontWeight.DISPLAY)
|
||||
self._dongle_id_text_label = MiciLabel(params.get("DongleId") or 'N/A', 32, width=max_width, color=subheader_color, font_weight=FontWeight.ROMAN)
|
||||
self._dongle_id_label = UnifiedLabel("device ID", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False)
|
||||
self._dongle_id_text_label = UnifiedLabel(params.get("DongleId") or 'N/A', 32, max_width=max_width, text_color=subheader_color,
|
||||
font_weight=FontWeight.ROMAN, wrap_text=False)
|
||||
|
||||
self._serial_number_label = MiciLabel("serial", 48, color=header_color, font_weight=FontWeight.DISPLAY)
|
||||
self._serial_number_text_label = MiciLabel(params.get("HardwareSerial") or 'N/A', 32, width=max_width, color=subheader_color, font_weight=FontWeight.ROMAN)
|
||||
self._serial_number_label = UnifiedLabel("serial", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False)
|
||||
self._serial_number_text_label = UnifiedLabel(params.get("HardwareSerial") or 'N/A', 32, max_width=max_width, text_color=subheader_color,
|
||||
font_weight=FontWeight.ROMAN, wrap_text=False)
|
||||
|
||||
def _render(self, _):
|
||||
self._dongle_id_label.set_position(self._rect.x + 20, self._rect.y - 10)
|
||||
@@ -135,7 +130,7 @@ class UpdaterState(IntEnum):
|
||||
|
||||
class PairBigButton(BigButton):
|
||||
def __init__(self):
|
||||
super().__init__("pair", "connect.comma.ai", "icons_mici/settings/comma_icon.png", icon_size=(33, 60))
|
||||
super().__init__("pair", "connect.comma.ai", gui_app.texture("icons_mici/settings/comma_icon.png", 33, 60))
|
||||
|
||||
def _get_label_font_size(self):
|
||||
return 64
|
||||
@@ -161,9 +156,9 @@ class PairBigButton(BigButton):
|
||||
return
|
||||
dlg: BigDialog | PairingDialog
|
||||
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):
|
||||
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:
|
||||
dlg = PairingDialog()
|
||||
gui_app.push_widget(dlg)
|
||||
@@ -193,7 +188,7 @@ class UpdateOpenpilotBigButton(BigButton):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
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)
|
||||
return
|
||||
|
||||
@@ -314,33 +309,33 @@ class DeviceLayoutMici(NavScroller):
|
||||
def uninstall_openpilot_callback():
|
||||
ui_state.params.put_bool("DoUninstall", True)
|
||||
|
||||
reset_calibration_btn = BigButton("reset calibration", "", "icons_mici/settings/device/lkas.png", icon_size=(114, 60))
|
||||
reset_calibration_btn.set_click_callback(lambda: _engaged_confirmation_callback(reset_calibration_callback, "reset"))
|
||||
reset_calibration_btn = EngagedConfirmationButton("reset calibration", "reset", gui_app.texture("icons_mici/settings/device/lkas.png", 122, 64),
|
||||
reset_calibration_callback)
|
||||
|
||||
uninstall_openpilot_btn = BigButton("uninstall sunnypilot", "", "icons_mici/settings/device/uninstall.png")
|
||||
uninstall_openpilot_btn.set_click_callback(lambda: _engaged_confirmation_callback(uninstall_openpilot_callback, "uninstall"))
|
||||
uninstall_openpilot_btn = EngagedConfirmationButton("uninstall sunnypilot", "uninstall",
|
||||
gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64),
|
||||
uninstall_openpilot_callback, exit_on_confirm=False)
|
||||
|
||||
reboot_btn = BigCircleButton("icons_mici/settings/device/reboot.png", red=False, icon_size=(64, 70))
|
||||
reboot_btn.set_click_callback(lambda: _engaged_confirmation_callback(reboot_callback, "reboot"))
|
||||
reboot_btn = EngagedConfirmationCircleButton("reboot", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70),
|
||||
reboot_callback, exit_on_confirm=False)
|
||||
|
||||
self._power_off_btn = BigCircleButton("icons_mici/settings/device/power.png", red=True, icon_size=(64, 66))
|
||||
self._power_off_btn.set_click_callback(lambda: _engaged_confirmation_callback(power_off_callback, "power off"))
|
||||
self._power_off_btn = EngagedConfirmationCircleButton("power off", gui_app.texture("icons_mici/settings/device/power.png", 64, 66),
|
||||
power_off_callback, exit_on_confirm=False, red=True)
|
||||
self._power_off_btn.set_visible(lambda: not ui_state.ignition)
|
||||
|
||||
regulatory_btn = BigButton("regulatory info", "", "icons_mici/settings/device/info.png")
|
||||
regulatory_btn = BigButton("regulatory info", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64))
|
||||
regulatory_btn.set_click_callback(self._on_regulatory)
|
||||
|
||||
driver_cam_btn = BigButton("driver\ncamera preview", "", "icons_mici/settings/device/cameras.png")
|
||||
driver_cam_btn = BigButton("driver\ncamera preview", "", gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64))
|
||||
driver_cam_btn.set_click_callback(lambda: gui_app.push_widget(DriverCameraDialog()))
|
||||
driver_cam_btn.set_enabled(lambda: ui_state.is_offroad())
|
||||
|
||||
review_training_guide_btn = BigButton("review\ntraining guide", "", "icons_mici/settings/device/info.png")
|
||||
review_training_guide_btn = BigButton("review\ntraining guide", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64))
|
||||
review_training_guide_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTrainingGuide(completed_callback=lambda: gui_app.pop_widgets_to(self))))
|
||||
review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad())
|
||||
|
||||
terms_btn = BigButton("terms &\nconditions", "", "icons_mici/settings/device/info.png")
|
||||
terms_btn = BigButton("terms &\nconditions", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64))
|
||||
terms_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTermsPage()))
|
||||
terms_btn.set_enabled(lambda: ui_state.is_offroad())
|
||||
|
||||
self._scroller.add_widgets([
|
||||
DeviceInfoLayoutMici(),
|
||||
|
||||
@@ -81,12 +81,12 @@ class FirehoseLayoutBase(Widget):
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# compute total content height for scrolling
|
||||
content_height = self._measure_content_height(rect)
|
||||
scroll_offset = round(self._scroll_panel.update(rect, content_height))
|
||||
scroll_offset = self._scroll_panel.update(rect, content_height)
|
||||
|
||||
# start drawing with offset
|
||||
x = int(rect.x + 40)
|
||||
y = int(rect.y + 40 + scroll_offset)
|
||||
w = int(rect.width - 80)
|
||||
x = rect.x + 40
|
||||
y = rect.y + 40 + scroll_offset
|
||||
w = rect.width - 80
|
||||
|
||||
# Title
|
||||
title_text = tr(TITLE)
|
||||
@@ -100,7 +100,7 @@ class FirehoseLayoutBase(Widget):
|
||||
y += 20
|
||||
|
||||
# Separator
|
||||
rl.draw_rectangle(x, y, w, 2, self.GRAY)
|
||||
rl.draw_rectangle_rec(rl.Rectangle(x, y, w, 2), self.GRAY)
|
||||
y += 20
|
||||
|
||||
# Status
|
||||
@@ -116,7 +116,7 @@ class FirehoseLayoutBase(Widget):
|
||||
y += 20
|
||||
|
||||
# Separator
|
||||
rl.draw_rectangle(x, y, w, 2, self.GRAY)
|
||||
rl.draw_rectangle_rec(rl.Rectangle(x, y, w, 2), self.GRAY)
|
||||
y += 20
|
||||
|
||||
# Instructions intro
|
||||
|
||||
@@ -3,9 +3,8 @@ import numpy as np
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog, BigConfirmationDialogV2
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog, BigConfirmationDialog
|
||||
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.widgets import Widget
|
||||
@@ -14,39 +13,26 @@ from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityT
|
||||
|
||||
|
||||
class LoadingAnimation(Widget):
|
||||
HIDE_TIME = 4
|
||||
RADIUS = 8
|
||||
SPACING = 24 # center-to-center: diameter (16) + gap (8)
|
||||
Y_MAG = 11.2
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._opacity_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
|
||||
self._opacity_target = 1.0
|
||||
self._hide_time = 0.0
|
||||
|
||||
def show_event(self):
|
||||
self._opacity_target = 1.0
|
||||
self._hide_time = rl.get_time()
|
||||
w = self.SPACING * 2 + self.RADIUS * 2
|
||||
h = self.RADIUS * 2 + int(self.Y_MAG)
|
||||
self.set_rect(rl.Rectangle(0, 0, w, h))
|
||||
|
||||
def _render(self, _):
|
||||
if rl.get_time() - self._hide_time > self.HIDE_TIME:
|
||||
self._opacity_target = 0.0
|
||||
|
||||
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
|
||||
# Balls rest at bottom center; bounce upward
|
||||
base_x = int(self._rect.x + self._rect.width / 2)
|
||||
base_y = int(self._rect.y + self._rect.height - self.RADIUS)
|
||||
|
||||
for i in range(3):
|
||||
x = cx - spacing + i * spacing
|
||||
y = int(cy + min(math.sin((rl.get_time() - i * 0.2) * anim_scale) * y_mag, 0))
|
||||
alpha = int(np.interp(cy - y, [0, y_mag], [255 * 0.45, 255 * 0.9]) * self._opacity_filter.x)
|
||||
rl.draw_circle(x, y, 5, rl.Color(255, 255, 255, alpha))
|
||||
x = base_x + (i - 1) * self.SPACING
|
||||
y = int(base_y + min(math.sin((rl.get_time() - i * 0.2) * 4) * self.Y_MAG, 0))
|
||||
alpha = int(np.interp(base_y - y, [0, self.Y_MAG], [255 * 0.45, 255 * 0.9]))
|
||||
rl.draw_circle(x, y, self.RADIUS, rl.Color(255, 255, 255, alpha))
|
||||
|
||||
|
||||
class WifiIcon(Widget):
|
||||
@@ -124,6 +110,10 @@ class WifiButton(BigButton):
|
||||
if self._is_connected or self._is_connecting:
|
||||
self._wrong_password = False
|
||||
|
||||
@property
|
||||
def network_forgetting(self) -> bool:
|
||||
return self._network_forgetting
|
||||
|
||||
def _forget_network(self):
|
||||
if self._network_forgetting:
|
||||
return
|
||||
@@ -175,7 +165,7 @@ class WifiButton(BigButton):
|
||||
|
||||
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)
|
||||
rl.draw_texture(self._check_txt, int(sub_label_x), check_y, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)))
|
||||
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)))
|
||||
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)
|
||||
@@ -256,8 +246,7 @@ class ForgetButton(Widget):
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
dlg = BigConfirmationDialogV2("slide to forget", "icons_mici/settings/network/new/trash.png", red=True,
|
||||
confirm_callback=self._forget_network)
|
||||
dlg = BigConfirmationDialog("slide to forget", gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64), self._forget_network, red=True)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
def _render(self, _):
|
||||
@@ -270,11 +259,26 @@ class ForgetButton(Widget):
|
||||
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):
|
||||
def __init__(self, wifi_manager: WifiManager):
|
||||
super().__init__()
|
||||
|
||||
self._loading_animation = LoadingAnimation()
|
||||
self._scanning_btn = ScanningButton()
|
||||
|
||||
self._wifi_manager = wifi_manager
|
||||
self._networks: dict[str, Network] = {}
|
||||
@@ -285,20 +289,23 @@ class WifiUIMici(NavScroller):
|
||||
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):
|
||||
# Clear scroller items and update from latest scan results
|
||||
# Re-sort scroller items and update from latest scan results
|
||||
super().show_event()
|
||||
self._loading_animation.show_event()
|
||||
self._wifi_manager.set_active(True)
|
||||
self._scroller.items.clear()
|
||||
# trigger button update on latest sorted networks
|
||||
self._on_network_updated(self._wifi_manager.networks)
|
||||
self._networks = {n.ssid: n for n in self._wifi_manager.networks}
|
||||
self._update_buttons(re_sort=True)
|
||||
|
||||
def _on_network_updated(self, networks: list[Network]):
|
||||
self._networks = {network.ssid: network for network in networks}
|
||||
self._update_buttons()
|
||||
|
||||
def _update_buttons(self):
|
||||
def _update_buttons(self, re_sort: bool = False):
|
||||
# Update existing buttons, add new ones to the end
|
||||
existing = {btn.network.ssid: btn for btn in self._scroller.items if isinstance(btn, WifiButton)}
|
||||
|
||||
@@ -310,11 +317,23 @@ class WifiUIMici(NavScroller):
|
||||
btn.set_click_callback(lambda ssid=network.ssid: self._connect_to_network(ssid))
|
||||
self._scroller.add_widget(btn)
|
||||
|
||||
if re_sort:
|
||||
# Remove stale buttons and sort to match scan order, preserving eager state
|
||||
btn_map = {btn.network.ssid: btn for btn in self._scroller.items if isinstance(btn, WifiButton)}
|
||||
self._scroller.items[:] = [btn_map[ssid] for ssid in self._networks if ssid in btn_map]
|
||||
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):
|
||||
self._wifi_manager.connect_to_network(ssid, password)
|
||||
self._move_network_to_front(ssid, scroll=True)
|
||||
@@ -370,17 +389,3 @@ class WifiUIMici(NavScroller):
|
||||
super()._update_state()
|
||||
|
||||
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))
|
||||
|
||||
@@ -20,23 +20,23 @@ class SettingsLayout(NavScroller):
|
||||
self._params = Params()
|
||||
|
||||
toggles_panel = TogglesLayoutMici()
|
||||
toggles_btn = SettingsBigButton("toggles", "", "icons_mici/settings.png")
|
||||
toggles_btn = SettingsBigButton("toggles", "", gui_app.texture("icons_mici/settings.png", 64, 64))
|
||||
toggles_btn.set_click_callback(lambda: gui_app.push_widget(toggles_panel))
|
||||
|
||||
network_panel = NetworkLayoutMici()
|
||||
network_btn = SettingsBigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png", icon_size=(76, 56))
|
||||
network_btn = SettingsBigButton("network", "", gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 76, 56))
|
||||
network_btn.set_click_callback(lambda: gui_app.push_widget(network_panel))
|
||||
|
||||
device_panel = DeviceLayoutMici()
|
||||
device_btn = SettingsBigButton("device", "", "icons_mici/settings/device_icon.png", icon_size=(74, 60))
|
||||
device_btn = SettingsBigButton("device", "", gui_app.texture("icons_mici/settings/device_icon.png", 72, 58))
|
||||
device_btn.set_click_callback(lambda: gui_app.push_widget(device_panel))
|
||||
|
||||
developer_panel = DeveloperLayoutMici()
|
||||
developer_btn = SettingsBigButton("developer", "", "icons_mici/settings/developer_icon.png", icon_size=(64, 60))
|
||||
developer_btn = SettingsBigButton("developer", "", gui_app.texture("icons_mici/settings/developer_icon.png", 64, 60))
|
||||
developer_btn.set_click_callback(lambda: gui_app.push_widget(developer_panel))
|
||||
|
||||
firehose_panel = FirehoseLayout()
|
||||
firehose_btn = SettingsBigButton("firehose", "", "icons_mici/settings/firehose.png", icon_size=(52, 62))
|
||||
firehose_btn = SettingsBigButton("firehose", "", gui_app.texture("icons_mici/settings/firehose.png", 52, 62))
|
||||
firehose_btn.set_click_callback(lambda: gui_app.push_widget(firehose_panel))
|
||||
|
||||
self._scroller.add_widgets([
|
||||
|
||||
@@ -272,7 +272,7 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
|
||||
else:
|
||||
icon_alpha = int(min(self._turn_signal_alpha_filter.x, 255))
|
||||
|
||||
rl.draw_texture(alert_layout.icon.texture, pos_x, int(self._rect.y + alert_layout.icon.margin_y),
|
||||
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.Color(255, 255, 255, int(icon_alpha * self._alpha_filter.x)))
|
||||
|
||||
def _draw_background(self, alert: Alert) -> None:
|
||||
|
||||
@@ -130,7 +130,7 @@ class BookmarkIcon(Widget):
|
||||
if self._offset_filter.x > 0:
|
||||
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
|
||||
rl.draw_texture(self._icon, int(icon_x), int(icon_y), rl.WHITE)
|
||||
rl.draw_texture_ex(self._icon, rl.Vector2(icon_x, icon_y), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
|
||||
class AugmentedRoadView(CameraView):
|
||||
|
||||
@@ -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)
|
||||
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_background = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_background.png", self._rect.width, self._rect.height)
|
||||
self._dm_background = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_background.png", int(self._rect.width), int(self._rect.height))
|
||||
|
||||
def set_should_draw(self, should_draw: bool):
|
||||
self._should_draw = should_draw
|
||||
@@ -88,14 +88,13 @@ class DriverStateRenderer(Widget):
|
||||
if DEBUG:
|
||||
rl.draw_rectangle_lines_ex(self._rect, 1, rl.RED)
|
||||
|
||||
rl.draw_texture(self._dm_background,
|
||||
int(self._rect.x),
|
||||
int(self._rect.y),
|
||||
rl.draw_texture_ex(self._dm_background,
|
||||
rl.Vector2(self._rect.x, self._rect.y), 0.0, 1.0,
|
||||
rl.Color(255, 255, 255, int(255 * self._fade_filter.x)))
|
||||
|
||||
rl.draw_texture(self._dm_person,
|
||||
int(self._rect.x + (self._rect.width - self._dm_person.width) / 2),
|
||||
int(self._rect.y + (self._rect.height - self._dm_person.height) / 2),
|
||||
rl.draw_texture_ex(self._dm_person,
|
||||
rl.Vector2(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,
|
||||
rl.Color(255, 255, 255, int(255 * 0.9 * self._fade_filter.x)))
|
||||
|
||||
if self.effective_active:
|
||||
|
||||
@@ -221,7 +221,7 @@ class HudRenderer(Widget):
|
||||
EXCLAMATION_POINT_SPACING = 10
|
||||
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
|
||||
rl.draw_texture(self._txt_exclamation_point, int(exclamation_pos_x), int(exclamation_pos_y), rl.WHITE)
|
||||
rl.draw_texture_ex(self._txt_exclamation_point, rl.Vector2(exclamation_pos_x, exclamation_pos_y), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
|
||||
"""Draw the MAX speed indicator box."""
|
||||
|
||||
@@ -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.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.dialog import BigDialog, BigConfirmationDialogV2, BigInputDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal
|
||||
|
||||
# tici dialogs
|
||||
@@ -44,7 +44,7 @@ KNOWN_LEAKS = {
|
||||
"openpilot.system.ui.widgets.scroller_tici.Scroller",
|
||||
"openpilot.system.ui.widgets.label.UnifiedLabel",
|
||||
"openpilot.system.ui.widgets.mici_keyboard.MiciKeyboard",
|
||||
"openpilot.selfdrive.ui.mici.widgets.dialog.BigConfirmationDialogV2",
|
||||
"openpilot.selfdrive.ui.mici.widgets.dialog.BigConfirmationDialog",
|
||||
"openpilot.system.ui.widgets.keyboard.Keyboard",
|
||||
"openpilot.system.ui.widgets.slider.BigSlider",
|
||||
"openpilot.selfdrive.ui.mici.widgets.dialog.BigInputDialog",
|
||||
@@ -72,7 +72,7 @@ def test_dialogs_do_not_leak():
|
||||
lambda: MiciTrainingGuide(lambda: None),
|
||||
lambda: MiciOnboardingWindow(lambda: None),
|
||||
lambda: BigDialog("test", "test"),
|
||||
lambda: BigConfirmationDialogV2("test", "icons_mici/settings/network/new/trash.png"),
|
||||
lambda: BigConfirmationDialog("test", gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64), lambda: None),
|
||||
lambda: BigInputDialog("test"),
|
||||
lambda: MiciFccModal(text="test"),
|
||||
# tici
|
||||
|
||||
@@ -28,7 +28,7 @@ class ScrollState(Enum):
|
||||
|
||||
|
||||
class BigCircleButton(Widget):
|
||||
def __init__(self, icon: str, red: bool = False, icon_size: tuple[int, int] = (64, 53), icon_offset: tuple[int, int] = (0, 0)):
|
||||
def __init__(self, icon: rl.Texture, red: bool = False, icon_offset: tuple[int, int] = (0, 0)):
|
||||
super().__init__()
|
||||
self._red = red
|
||||
self._icon_offset = icon_offset
|
||||
@@ -39,7 +39,7 @@ class BigCircleButton(Widget):
|
||||
self._click_delay = 0.075
|
||||
|
||||
# Icons
|
||||
self._txt_icon = gui_app.texture(icon, *icon_size)
|
||||
self._txt_icon = icon
|
||||
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)
|
||||
@@ -71,8 +71,8 @@ class BigCircleButton(Widget):
|
||||
|
||||
|
||||
class BigCircleToggle(BigCircleButton):
|
||||
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_size=icon_size, icon_offset=icon_offset)
|
||||
def __init__(self, icon: rl.Texture, toggle_callback: Callable | None = None, icon_offset: tuple[int, int] = (0, 0)):
|
||||
super().__init__(icon, False, icon_offset=icon_offset)
|
||||
self._toggle_callback = toggle_callback
|
||||
|
||||
# State
|
||||
@@ -107,15 +107,13 @@ class BigButton(Widget):
|
||||
|
||||
"""A lightweight stand-in for the Qt BigButton, drawn & updated each frame."""
|
||||
|
||||
def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", icon_size: tuple[int, int] = (64, 64),
|
||||
scroll: bool = False):
|
||||
def __init__(self, text: str, value: str = "", icon: Union[rl.Texture, None] = None, scroll: bool = False):
|
||||
super().__init__()
|
||||
self.set_rect(rl.Rectangle(0, 0, 402, 180))
|
||||
self.text = text
|
||||
self.value = value
|
||||
self._icon_size = icon_size
|
||||
self._txt_icon = icon
|
||||
self._scroll = scroll
|
||||
self.set_icon(icon)
|
||||
|
||||
self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps)
|
||||
self._click_delay = 0.075
|
||||
@@ -133,8 +131,8 @@ class BigButton(Widget):
|
||||
|
||||
self._load_images()
|
||||
|
||||
def set_icon(self, icon: Union[str, rl.Texture]):
|
||||
self._txt_icon = gui_app.texture(icon, *self._icon_size) if isinstance(icon, str) and len(icon) else icon
|
||||
def set_icon(self, icon: Union[rl.Texture, None]):
|
||||
self._txt_icon = icon
|
||||
|
||||
def set_rotate_icon(self, rotate: bool):
|
||||
if rotate and self._rotate_icon_t is not None:
|
||||
@@ -151,7 +149,7 @@ class BigButton(Widget):
|
||||
|
||||
def _width_hint(self) -> int:
|
||||
# Single line if scrolling, so hide behind icon if exists
|
||||
icon_size = self._icon_size[0] if self._txt_icon and self._scroll and self.value else 0
|
||||
icon_size = self._txt_icon.width if self._txt_icon and self._scroll and self.value else 0
|
||||
return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2 - icon_size)
|
||||
|
||||
def _get_label_font_size(self):
|
||||
@@ -194,7 +192,9 @@ class BigButton(Widget):
|
||||
SHAKE_DURATION = 0.5
|
||||
SHAKE_AMPLITUDE = 24.0
|
||||
SHAKE_FREQUENCY = 32.0
|
||||
t = rl.get_time() - (self._shake_start or 0.0)
|
||||
if self._shake_start is None:
|
||||
return 0.0
|
||||
t = rl.get_time() - self._shake_start
|
||||
if t > SHAKE_DURATION:
|
||||
return 0.0
|
||||
decay = 1.0 - t / SHAKE_DURATION
|
||||
@@ -335,6 +335,43 @@ class BigMultiToggle(BigToggle):
|
||||
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):
|
||||
def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable | None = None,
|
||||
select_callback: Callable | None = None):
|
||||
@@ -370,9 +407,9 @@ class BigParamControl(BigToggle):
|
||||
|
||||
# TODO: param control base class
|
||||
class BigCircleParamControl(BigCircleToggle):
|
||||
def __init__(self, icon: str, param: str, toggle_callback: Callable | None = None, icon_size: tuple[int, int] = (64, 53),
|
||||
def __init__(self, icon: rl.Texture, param: str, toggle_callback: Callable | None = None,
|
||||
icon_offset: tuple[int, int] = (0, 0)):
|
||||
super().__init__(icon, toggle_callback, icon_size=icon_size, icon_offset=icon_offset)
|
||||
super().__init__(icon, toggle_callback, icon_offset=icon_offset)
|
||||
self._param = param
|
||||
self.params = Params()
|
||||
self.set_checked(self.params.get_bool(self._param, False))
|
||||
|
||||
@@ -4,14 +4,13 @@ import pyray as rl
|
||||
from typing import Union
|
||||
from collections.abc import Callable
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
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.wrap_text import wrap_text
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.widgets.slider import RedBigSlider, BigSlider
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton, BigButton, GreyBigButton
|
||||
|
||||
DEBUG = False
|
||||
|
||||
@@ -25,58 +24,31 @@ class BigDialogBase(NavWidget, abc.ABC):
|
||||
|
||||
|
||||
class BigDialog(BigDialogBase):
|
||||
def __init__(self,
|
||||
title: str,
|
||||
description: str):
|
||||
def __init__(self, title: str, description: str, icon: Union[rl.Texture, None] = None):
|
||||
super().__init__()
|
||||
self._title = title
|
||||
self._description = description
|
||||
self._card = GreyBigButton(title, description, icon)
|
||||
|
||||
def _render(self, _):
|
||||
super()._render(_)
|
||||
|
||||
# draw title
|
||||
# TODO: we desperately need layouts
|
||||
# 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)
|
||||
self._card.render(rl.Rectangle(
|
||||
self._rect.x + self._rect.width / 2 - self._card.rect.width / 2,
|
||||
self._rect.y + self._rect.height / 2 - self._card.rect.height / 2,
|
||||
self._card.rect.width,
|
||||
self._card.rect.height,
|
||||
))
|
||||
|
||||
|
||||
class BigConfirmationDialogV2(BigDialogBase):
|
||||
def __init__(self, title: str, icon: str, red: bool = False,
|
||||
exit_on_confirm: bool = True,
|
||||
confirm_callback: Callable | None = None):
|
||||
class BigConfirmationDialog(BigDialogBase):
|
||||
def __init__(self, title: str, icon: rl.Texture, confirm_callback: Callable[[], None],
|
||||
exit_on_confirm: bool = True, red: bool = False):
|
||||
super().__init__()
|
||||
self._confirm_callback = confirm_callback
|
||||
self._exit_on_confirm = exit_on_confirm
|
||||
|
||||
icon_txt = gui_app.texture(icon, 64, 53)
|
||||
self._slider: BigSlider | RedBigSlider
|
||||
if red:
|
||||
self._slider = RedBigSlider(title, icon_txt, confirm_callback=self._on_confirm)
|
||||
self._slider = self._child(RedBigSlider(title, icon, confirm_callback=self._on_confirm))
|
||||
else:
|
||||
self._slider = BigSlider(title, icon_txt, confirm_callback=self._on_confirm)
|
||||
self._slider = self._child(BigSlider(title, icon, confirm_callback=self._on_confirm))
|
||||
self._slider.set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack + NavWidget
|
||||
|
||||
def _on_confirm(self):
|
||||
@@ -158,9 +130,9 @@ class BigInputDialog(BigDialogBase):
|
||||
|
||||
bg_block_margin = 5
|
||||
text_x = PADDING / 2 + self._enter_img.width + PADDING
|
||||
text_field_rect = rl.Rectangle(text_x, int(self._rect.y + PADDING) - bg_block_margin,
|
||||
int(self._rect.width - text_x * 2),
|
||||
int(text_size.y))
|
||||
text_field_rect = rl.Rectangle(text_x, self._rect.y + PADDING - bg_block_margin,
|
||||
self._rect.width - text_x * 2,
|
||||
text_size.y)
|
||||
|
||||
# draw text input
|
||||
# push text left with a gradient on left side if too long
|
||||
@@ -181,8 +153,8 @@ class BigInputDialog(BigDialogBase):
|
||||
|
||||
# draw gradient on left side to indicate more text
|
||||
if text_size.x > text_field_rect.width:
|
||||
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.draw_rectangle_gradient_ex(rl.Rectangle(text_field_rect.x, text_field_rect.y, 80, text_field_rect.height),
|
||||
rl.BLACK, rl.BLANK, rl.BLANK, rl.BLACK)
|
||||
|
||||
# draw cursor
|
||||
blink_alpha = (math.sin(rl.get_time() * 6) + 1) / 2
|
||||
@@ -190,14 +162,14 @@ class BigInputDialog(BigDialogBase):
|
||||
cursor_x = min(text_x + text_size.x + 3, text_field_rect.x + text_field_rect.width)
|
||||
else:
|
||||
cursor_x = text_field_rect.x - 6
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(int(cursor_x), int(text_field_rect.y), 4, int(text_size.y)),
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(cursor_x, text_field_rect.y, 4, text_size.y),
|
||||
1, 4, rl.Color(255, 255, 255, int(255 * blink_alpha)))
|
||||
|
||||
# draw backspace icon with nice fade
|
||||
self._backspace_img_alpha.update(255 * bool(text))
|
||||
if self._backspace_img_alpha.x > 1:
|
||||
color = rl.Color(255, 255, 255, int(self._backspace_img_alpha.x))
|
||||
rl.draw_texture(self._backspace_img, int(self._rect.width - self._backspace_img.width - 27), int(self._rect.y + 14), color)
|
||||
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)
|
||||
|
||||
if not text and self._hint_label.text and not candidate_char:
|
||||
# draw description if no text entered yet and not drawing candidate char
|
||||
@@ -215,9 +187,9 @@ class BigInputDialog(BigDialogBase):
|
||||
# draw enter button
|
||||
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))
|
||||
rl.draw_texture(self._enter_img, int(self._rect.x + PADDING / 2), int(self._rect.y), color)
|
||||
rl.draw_texture_ex(self._enter_img, rl.Vector2(self._rect.x + PADDING / 2, self._rect.y), 0.0, 1.0, color)
|
||||
color = rl.Color(255, 255, 255, 255 - int(self._enter_img_alpha.x))
|
||||
rl.draw_texture(self._enter_disabled_img, int(self._rect.x + PADDING / 2), int(self._rect.y), color)
|
||||
rl.draw_texture_ex(self._enter_disabled_img, rl.Vector2(self._rect.x + PADDING / 2, self._rect.y), 0.0, 1.0, color)
|
||||
|
||||
# keyboard goes over everything
|
||||
self._keyboard.render(self._rect)
|
||||
@@ -254,3 +226,15 @@ class BigDialogButton(BigButton):
|
||||
|
||||
dlg = BigDialog(self.text, self._description)
|
||||
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)
|
||||
|
||||
@@ -9,7 +9,7 @@ from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.widgets.label import MiciLabel
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
|
||||
|
||||
class PairingDialog(NavWidget):
|
||||
@@ -24,8 +24,7 @@ class PairingDialog(NavWidget):
|
||||
self._last_qr_generation = float("-inf")
|
||||
|
||||
self._txt_pair = gui_app.texture("icons_mici/settings/device/pair.png", 33, 60)
|
||||
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)
|
||||
self._pair_label = UnifiedLabel("pair with comma connect", font_size=48, font_weight=FontWeight.BOLD, line_height=0.8)
|
||||
|
||||
def _get_pairing_url(self) -> str:
|
||||
try:
|
||||
@@ -77,7 +76,7 @@ class PairingDialog(NavWidget):
|
||||
self._render_qr_code()
|
||||
|
||||
label_x = self._rect.x + 8 + self._rect.height + 24
|
||||
self._pair_label.set_width(int(self._rect.width - label_x))
|
||||
self._pair_label.set_max_width(int(self._rect.width - label_x))
|
||||
self._pair_label.set_position(label_x, self._rect.y + 16)
|
||||
self._pair_label.render()
|
||||
|
||||
@@ -93,7 +92,7 @@ class PairingDialog(NavWidget):
|
||||
return
|
||||
|
||||
scale = self._rect.height / self._qr_texture.height
|
||||
pos = rl.Vector2(self._rect.x + 8, self._rect.y)
|
||||
pos = rl.Vector2(round(self._rect.x + 8), round(self._rect.y))
|
||||
rl.draw_texture_ex(self._qr_texture, pos, 0.0, scale, rl.WHITE)
|
||||
|
||||
def __del__(self):
|
||||
|
||||
@@ -50,7 +50,7 @@ class ExpButton(Widget):
|
||||
|
||||
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_texture(texture, center_x - texture.width // 2, center_y - texture.height // 2, self._white_color)
|
||||
rl.draw_texture_ex(texture, rl.Vector2(center_x - texture.width / 2, center_y - texture.height / 2), 0.0, 1.0, self._white_color)
|
||||
|
||||
def _held_or_actual_mode(self):
|
||||
now = time.monotonic()
|
||||
|
||||
@@ -20,7 +20,7 @@ class SunnylinkConsentPage(Widget):
|
||||
self._done_callback = done_callback
|
||||
self._step = 0
|
||||
|
||||
self._title = Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT))
|
||||
|
||||
self._content = [
|
||||
{
|
||||
@@ -40,9 +40,10 @@ class SunnylinkConsentPage(Widget):
|
||||
}
|
||||
]
|
||||
|
||||
self._primary_btn = Button("", button_style=ButtonStyle.PRIMARY, click_callback=lambda: self._handle_choice("enable"))
|
||||
self._secondary_btn = Button("", button_style=ButtonStyle.NORMAL, click_callback=lambda: self._handle_choice("secondary"))
|
||||
self._danger_btn = Button("", button_style=ButtonStyle.DANGER, click_callback=lambda: self._handle_choice("disable"))
|
||||
self._primary_btn = self._child(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._danger_btn = self._child(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):
|
||||
if choice == "enable":
|
||||
@@ -73,8 +74,8 @@ class SunnylinkConsentPage(Widget):
|
||||
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_label = Label(step_data["text"], font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
desc_label.render(desc_rect)
|
||||
self._desc.set_text(step_data["text"])
|
||||
self._desc.render(desc_rect)
|
||||
|
||||
btn_y = self._rect.y + self._rect.height - 160 - 45
|
||||
|
||||
|
||||
@@ -82,8 +82,7 @@ class NavButton(Widget):
|
||||
|
||||
if self.panel_info.icon:
|
||||
icon_texture = gui_app.texture(self.panel_info.icon, ICON_SIZE, ICON_SIZE, keep_aspect_ratio=True)
|
||||
rl.draw_texture(icon_texture, int(content_x), int(rect.y + (OP.NAV_BTN_HEIGHT - icon_texture.height) / 2),
|
||||
rl.WHITE)
|
||||
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)
|
||||
content_x += ICON_SIZE + 20
|
||||
|
||||
# Draw button text (right-aligned)
|
||||
|
||||
@@ -41,7 +41,7 @@ class SunnylinkHeader(Widget):
|
||||
self._description = UnifiedLabel(
|
||||
text=tr("For secure backup, restore, and remote configuration"),
|
||||
font_size=40,
|
||||
font_weight=FontWeight.LIGHT,
|
||||
font_weight=FontWeight.NORMAL,
|
||||
text_color=rl.Color(0, 255, 0, 255), # Green
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
@@ -53,7 +53,7 @@ class SunnylinkHeader(Widget):
|
||||
text=tr("Sponsorship isn't required for basic backup/restore") + "\n" +
|
||||
tr("Click the Sponsor button for more details"),
|
||||
font_size=35,
|
||||
font_weight=FontWeight.LIGHT,
|
||||
font_weight=FontWeight.NORMAL,
|
||||
text_color=rl.Color(255, 165, 0, 255), # Orange
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
@@ -107,7 +107,7 @@ class SunnylinkDescriptionItem(Widget):
|
||||
self._description = UnifiedLabel(
|
||||
text="",
|
||||
font_size=40,
|
||||
font_weight=FontWeight.LIGHT,
|
||||
font_weight=FontWeight.NORMAL,
|
||||
text_color=rl.WHITE,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
|
||||
@@ -93,7 +93,7 @@ class TripsLayout(Widget):
|
||||
|
||||
# Values
|
||||
number_font = gui_app.font(FontWeight.BOLD)
|
||||
unit_font = gui_app.font(FontWeight.LIGHT)
|
||||
unit_font = gui_app.font(FontWeight.NORMAL)
|
||||
number_base_size = 92
|
||||
unit_base_size = 55
|
||||
number_size = number_base_size * FONT_SCALE
|
||||
@@ -112,9 +112,9 @@ class TripsLayout(Widget):
|
||||
center_x = col_x + (col_width / 2)
|
||||
|
||||
# Icon
|
||||
icon_x = int(center_x - (icon.width / 2))
|
||||
icon_y = int(content_y + 60)
|
||||
rl.draw_texture(icon, icon_x, icon_y, rl.WHITE)
|
||||
icon_x = center_x - (icon.width / 2)
|
||||
icon_y = content_y + 60
|
||||
rl.draw_texture_ex(icon, rl.Vector2(icon_x, icon_y), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
# Value
|
||||
val_size = measure_text_cached(number_font, value, number_base_size)
|
||||
|
||||
@@ -20,10 +20,10 @@ class ModelsLayoutMici(NavScroller):
|
||||
self.original_back_callback = back_callback
|
||||
self.focused_widget = None
|
||||
|
||||
self.current_model_btn = BigButton(tr("current model"), "", "")
|
||||
self.current_model_btn = BigButton(tr("current model"))
|
||||
self.current_model_btn.set_click_callback(self._show_folders)
|
||||
|
||||
self.cancel_download_btn = BigButton(tr("cancel download"), "", "")
|
||||
self.cancel_download_btn = BigButton(tr("cancel download"))
|
||||
self.cancel_download_btn.set_click_callback(lambda: ui_state.params.remove("ModelManager_DownloadIndex"))
|
||||
|
||||
self.main_items = [self.current_model_btn, self.cancel_download_btn]
|
||||
@@ -52,13 +52,13 @@ class ModelsLayoutMici(NavScroller):
|
||||
self.focused_widget = self.current_model_btn
|
||||
folders = self._get_grouped_bundles()
|
||||
folder_buttons = []
|
||||
default_btn = BigButton(tr("default model"), "", "")
|
||||
default_btn = BigButton(tr("default model"))
|
||||
default_btn.set_click_callback(self._select_default)
|
||||
folder_buttons.append(default_btn)
|
||||
|
||||
for folder in sorted(folders.keys(), key=lambda f: max((bundle.index for bundle in folders[f]), default=-1), reverse=True):
|
||||
if folder.lower() in ["release models", "master models"]:
|
||||
btn = BigButton(folder.lower(), "", "")
|
||||
btn = BigButton(folder.lower())
|
||||
btn.set_click_callback(lambda f=folder: self._select_folder(f))
|
||||
folder_buttons.append(btn)
|
||||
self._show_selection_view(folder_buttons, self._reset_main_view)
|
||||
@@ -78,7 +78,7 @@ class ModelsLayoutMici(NavScroller):
|
||||
btns = []
|
||||
for bundle in bundles:
|
||||
txt = bundle.displayName.lower()
|
||||
btn = BigButton(txt, "", "")
|
||||
btn = BigButton(txt)
|
||||
btn.set_click_callback(lambda b=bundle: self._select_model(b))
|
||||
btns.append(btn)
|
||||
self._show_selection_view(btns, self._show_folders)
|
||||
|
||||
@@ -6,8 +6,7 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialogV2
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationCircleButton
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.mici_setup import GreyBigButton
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
@@ -17,19 +16,11 @@ class SunnylinkConsentPage(NavScroller):
|
||||
def __init__(self, on_accept: Callable | None = None, on_decline: Callable | None = None):
|
||||
super().__init__()
|
||||
|
||||
def show_accept_dialog():
|
||||
gui_app.push_widget(BigConfirmationDialogV2("enable\nsunnylink", "icons_mici/setup/driver_monitoring/dm_check.png",
|
||||
confirm_callback=on_accept))
|
||||
self._accept_button = BigConfirmationCircleButton("enable\nsunnylink", gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 64, 64),
|
||||
on_accept, exit_on_confirm=False)
|
||||
|
||||
def show_decline_dialog():
|
||||
gui_app.push_widget(BigConfirmationDialogV2("disable\nsunnylink", "icons_mici/setup/cancel.png",
|
||||
red=True, confirm_callback=on_decline))
|
||||
|
||||
self._accept_button = BigCircleButton("icons_mici/setup/driver_monitoring/dm_check.png")
|
||||
self._accept_button.set_click_callback(show_accept_dialog)
|
||||
|
||||
self._decline_button = BigCircleButton("icons_mici/setup/cancel.png", red=True)
|
||||
self._decline_button.set_click_callback(show_decline_dialog)
|
||||
self._decline_button = BigConfirmationCircleButton("disable\nsunnylink", gui_app.texture("icons_mici/setup/cancel.png", 64, 64),
|
||||
on_decline, red=True, exit_on_confirm=False)
|
||||
|
||||
self._scroller.add_widgets([
|
||||
GreyBigButton("sunnylink", "scroll to continue",
|
||||
|
||||
@@ -18,11 +18,11 @@ class SettingsLayoutSP(OP.SettingsLayout):
|
||||
OP.SettingsLayout.__init__(self)
|
||||
|
||||
sunnylink_panel = SunnylinkLayoutMici(back_callback=gui_app.pop_widget)
|
||||
sunnylink_btn = BigButton("sunnylink", "", "icons_mici/settings/developer/ssh.png")
|
||||
sunnylink_btn = BigButton("sunnylink", "", gui_app.texture("icons_mici/settings/developer/ssh.png", ICON_SIZE, ICON_SIZE))
|
||||
sunnylink_btn.set_click_callback(lambda: gui_app.push_widget(sunnylink_panel))
|
||||
|
||||
models_panel = ModelsLayoutMici(back_callback=gui_app.pop_widget)
|
||||
models_btn = BigButton("models", "", "../../sunnypilot/selfdrive/assets/offroad/icon_models.png")
|
||||
models_btn = BigButton("models", "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_models.png", ICON_SIZE, ICON_SIZE))
|
||||
models_btn.set_click_callback(lambda: gui_app.push_widget(models_panel))
|
||||
|
||||
items = self._scroller._items.copy()
|
||||
|
||||
@@ -8,7 +8,7 @@ from collections.abc import Callable
|
||||
|
||||
from cereal import custom
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkConsentPage
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.sunnylink_pairing_dialog import SunnylinkPairingDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
@@ -32,9 +32,9 @@ class SunnylinkLayoutMici(NavScroller):
|
||||
toggle_callback=self._sunnylink_toggle_callback)
|
||||
self._sunnylink_sponsor_button = SunnylinkPairBigButton(sponsor_pairing=False)
|
||||
self._sunnylink_pair_button = SunnylinkPairBigButton(sponsor_pairing=True)
|
||||
self._backup_btn = BigButton(tr("backup settings"), "", "")
|
||||
self._backup_btn = BigButton(tr("backup settings"), "")
|
||||
self._backup_btn.set_click_callback(lambda: self._handle_backup_restore_btn(restore=False))
|
||||
self._restore_btn = BigButton(tr("restore settings"), "", "")
|
||||
self._restore_btn = BigButton(tr("restore settings"), "")
|
||||
self._restore_btn.set_click_callback(lambda: self._handle_backup_restore_btn(restore=True))
|
||||
self._sunnylink_uploader_toggle = BigToggle(text=tr("sunnylink uploader"), initial_state=False,
|
||||
toggle_callback=self._sunnylink_uploader_callback)
|
||||
@@ -105,8 +105,8 @@ class SunnylinkLayoutMici(NavScroller):
|
||||
|
||||
def _handle_backup_restore_btn(self, restore: bool = False):
|
||||
lbl = tr("slide to restore") if restore else tr("slide to backup")
|
||||
icon = "icons_mici/settings/device/update.png"
|
||||
dlg = BigConfirmationDialogV2(lbl, icon, confirm_callback=self._restore_handler if restore else self._backup_handler)
|
||||
icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 64)
|
||||
dlg = BigConfirmationDialog(lbl, icon, confirm_callback=self._restore_handler if restore else self._backup_handler)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
def _backup_handler(self):
|
||||
@@ -169,8 +169,8 @@ class SunnylinkLayoutMici(NavScroller):
|
||||
elif (restore_status == custom.BackupManagerSP.Status.completed or
|
||||
(restore_status == custom.BackupManagerSP.Status.idle and restore_progress == 100.0)):
|
||||
self._restore_in_progress = False
|
||||
gui_app.push_widget(BigConfirmationDialogV2(
|
||||
title="slide to restart", icon="icons_mici/settings/device/reboot.png",
|
||||
gui_app.push_widget(BigConfirmationDialog(
|
||||
title="slide to restart", icon=gui_app.texture("icons_mici/settings/device/reboot.png", 64, 64),
|
||||
confirm_callback=lambda: gui_app.request_close()))
|
||||
|
||||
else:
|
||||
@@ -186,7 +186,7 @@ class SunnylinkLayoutMici(NavScroller):
|
||||
class SunnylinkPairBigButton(BigButton):
|
||||
def __init__(self, sponsor_pairing: bool = False):
|
||||
self.sponsor_pairing = sponsor_pairing
|
||||
super().__init__("", "", "")
|
||||
super().__init__("")
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
@@ -13,7 +13,7 @@ from openpilot.sunnypilot.sunnylink.api import SunnylinkApi, UNREGISTERED_SUNNYL
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
from openpilot.system.ui.widgets.label import MiciLabel
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
|
||||
|
||||
class SunnylinkPairingDialog(PairingDialog):
|
||||
@@ -23,8 +23,8 @@ class SunnylinkPairingDialog(PairingDialog):
|
||||
PairingDialog.__init__(self)
|
||||
self._sponsor_pairing = sponsor_pairing
|
||||
label_text = tr("pair with sunnylink") if sponsor_pairing else tr("become a sunnypilot sponsor")
|
||||
self._pair_label = MiciLabel(label_text, 48, font_weight=FontWeight.BOLD,
|
||||
color=rl.Color(255, 255, 255, int(255 * 0.9)), line_height=40, wrap_text=True)
|
||||
self._pair_label = UnifiedLabel(label_text, font_size=48, font_weight=FontWeight.BOLD,
|
||||
text_color=rl.Color(255, 255, 255, int(255 * 0.9)), line_height=0.8)
|
||||
|
||||
def _get_pairing_url(self) -> str:
|
||||
qr_string = "https://github.com/sponsors/sunnyhaibin"
|
||||
|
||||
@@ -42,11 +42,11 @@ class BlindSpotIndicators:
|
||||
pos_y = int(rect.y + BLIND_SPOT_Y_OFFSET)
|
||||
alpha = int(255 * self._blind_spot_left_alpha_filter.x)
|
||||
color = rl.Color(255, 255, 255, alpha)
|
||||
rl.draw_texture(self._txt_blind_spot_left, pos_x, pos_y, color)
|
||||
rl.draw_texture_ex(self._txt_blind_spot_left, rl.Vector2(pos_x, pos_y), 0.0, 1.0, color)
|
||||
|
||||
if self._blind_spot_right_alpha_filter.x > 0.01:
|
||||
pos_x = int(rect.x + rect.width - BLIND_SPOT_MARGIN_X - self._txt_blind_spot_right.width)
|
||||
pos_y = int(rect.y + BLIND_SPOT_Y_OFFSET)
|
||||
alpha = int(255 * self._blind_spot_right_alpha_filter.x)
|
||||
color = rl.Color(255, 255, 255, alpha)
|
||||
rl.draw_texture(self._txt_blind_spot_right, pos_x, pos_y, color)
|
||||
rl.draw_texture_ex(self._txt_blind_spot_right, rl.Vector2(pos_x, pos_y), 0.0, 1.0, color)
|
||||
|
||||
@@ -101,9 +101,9 @@ class CircularAlertsRenderer:
|
||||
|
||||
# Draw Image
|
||||
if self._alert_img and self._e2e_alert_display_timer > 0:
|
||||
img_x = int(center.x - self._alert_img.width / 2)
|
||||
img_y = int(center.y - self._alert_img.height / 2)
|
||||
rl.draw_texture(self._alert_img, img_x, img_y, rl.WHITE)
|
||||
img_x = center.x - self._alert_img.width / 2
|
||||
img_y = center.y - self._alert_img.height / 2
|
||||
rl.draw_texture_ex(self._alert_img, rl.Vector2(img_x, img_y), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
# Draw Text
|
||||
txt_color = rl.Color(255, 255, 255, 255) if is_pulsing else rl.Color(0, 255, 0, 190)
|
||||
|
||||
@@ -227,7 +227,7 @@ class SpeedLimitRenderer(Widget, SpeedLimitAlertRenderer):
|
||||
arrow_x = sign_rect.x + sign_rect.width + arrow_spacing
|
||||
arrow_y = sign_rect.y + (sign_rect.height - txt_icon.height) / 2
|
||||
color = rl.Color(255, 255, 255, int(icon_alpha))
|
||||
rl.draw_texture(txt_icon, int(arrow_x), int(arrow_y), color)
|
||||
rl.draw_texture_ex(txt_icon, rl.Vector2(arrow_x, arrow_y), 0.0, 1.0, color)
|
||||
|
||||
def _render_vienna(self, rect, val, sub, color, has_limit, alpha=1.0):
|
||||
center = rl.Vector2(rect.x + rect.width / 2, rect.y + rect.height / 2)
|
||||
|
||||
@@ -55,10 +55,10 @@ class TurnSignalWidget(Widget):
|
||||
self._texture = self._blind_spot_texture if self._type == 'blind_spot' else self._signal_texture
|
||||
|
||||
if self._texture:
|
||||
pos_x = int(self._rect.x + (self._rect.width - self._texture.width) / 2)
|
||||
pos_y = int(self._rect.y + (self._rect.height - self._texture.height) / 2)
|
||||
pos_x = self._rect.x + (self._rect.width - self._texture.width) / 2
|
||||
pos_y = self._rect.y + (self._rect.height - self._texture.height) / 2
|
||||
color = rl.Color(255, 255, 255, icon_alpha)
|
||||
rl.draw_texture(self._texture, pos_x, pos_y, color)
|
||||
rl.draw_texture_ex(self._texture, rl.Vector2(pos_x, pos_y), 0.0, 1.0, color)
|
||||
|
||||
def activate(self, _type: str = 'signal'):
|
||||
if not self._active or self._type != _type:
|
||||
|
||||
2
selfdrive/ui/tests/.gitignore
vendored
2
selfdrive/ui/tests/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
test
|
||||
test_translations
|
||||
2
selfdrive/ui/tests/diff/.gitignore
vendored
2
selfdrive/ui/tests/diff/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
report
|
||||
.coverage
|
||||
@@ -4,12 +4,16 @@ import argparse
|
||||
import coverage
|
||||
import pyray as rl
|
||||
|
||||
from tqdm import tqdm
|
||||
from typing import Literal
|
||||
from collections.abc import Callable
|
||||
from cereal.messaging import PubMaster
|
||||
from openpilot.common.api import Api
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.selfdrive.ui.tests.diff.diff import DIFF_OUT_DIR
|
||||
from openpilot.system.updated.updated import parse_release_notes
|
||||
from openpilot.system.version import terms_version, training_version, terms_version_sp, sunnylink_consent_version
|
||||
|
||||
LayoutVariant = Literal["mici", "tizi"]
|
||||
@@ -25,6 +29,7 @@ def setup_state():
|
||||
params.put("DongleId", "test123456789")
|
||||
# Combined description for layouts that still use it (BIG home, settings/software)
|
||||
params.put("UpdaterCurrentDescription", "0.10.1 / test-branch / abc1234 / Nov 30")
|
||||
params.put("UpdaterCurrentReleaseNotes", parse_release_notes(BASEDIR))
|
||||
params.put("HasAcceptedTermsSP", terms_version_sp)
|
||||
params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
|
||||
|
||||
@@ -34,6 +39,9 @@ def setup_state():
|
||||
params.put("GitCommit", "abc12340ff9131237ba23a1d0fbd8edf9c80e87")
|
||||
params.put("GitCommitDate", "'1732924800 2024-11-30 00:00:00 +0000'")
|
||||
|
||||
# Patch Api.get_token to return a static token so the pairing QR code is deterministic across runs
|
||||
Api.get_token = lambda self, payload_extra=None, expiry_hours=0: "test_token"
|
||||
|
||||
|
||||
def run_replay(variant: LayoutVariant) -> None:
|
||||
if HEADLESS:
|
||||
@@ -43,7 +51,7 @@ def run_replay(variant: LayoutVariant) -> None:
|
||||
setup_state()
|
||||
os.makedirs(DIFF_OUT_DIR, exist_ok=True)
|
||||
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state # Import within OpenpilotPrefix context so param values are setup correctly
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device # Import within OpenpilotPrefix context so param values are setup correctly
|
||||
from openpilot.system.ui.lib.application import gui_app # Import here for accurate coverage
|
||||
from openpilot.selfdrive.ui.tests.diff.replay_script import build_script
|
||||
|
||||
@@ -56,6 +64,10 @@ def run_replay(variant: LayoutVariant) -> None:
|
||||
from openpilot.selfdrive.ui.layouts.main import MainLayout
|
||||
main_layout = MainLayout()
|
||||
|
||||
# Disable interactive timeout — replay clicks use left_down=False so they never reset the timer,
|
||||
# and after 30s of real wall-clock time the settings panel would close automatically.
|
||||
device.set_override_interactive_timeout(99999)
|
||||
|
||||
pm = PubMaster(["deviceState", "pandaStates", "driverStateV2", "selfdriveState"])
|
||||
script = build_script(pm, main_layout, variant)
|
||||
script_index = 0
|
||||
@@ -67,6 +79,7 @@ def run_replay(variant: LayoutVariant) -> None:
|
||||
rl.get_time = lambda: frame / FPS
|
||||
|
||||
# Main loop to replay events and render frames
|
||||
with tqdm(total=script[-1][0] + 1, desc="Replaying", unit="frame", disable=bool(os.getenv("CI"))) as pbar:
|
||||
for _ in gui_app.render():
|
||||
# Handle all events for the current frame
|
||||
while script_index < len(script) and script[script_index][0] == frame:
|
||||
@@ -91,6 +104,7 @@ def run_replay(variant: LayoutVariant) -> None:
|
||||
ui_state.update()
|
||||
|
||||
frame += 1
|
||||
pbar.update(1)
|
||||
|
||||
if script_index >= len(script):
|
||||
break
|
||||
|
||||
@@ -3,15 +3,27 @@ from typing import TYPE_CHECKING
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
import math
|
||||
|
||||
from cereal import car, log, messaging
|
||||
from cereal.messaging import PubMaster
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
|
||||
from openpilot.selfdrive.ui.tests.diff.replay import FPS, LayoutVariant
|
||||
from openpilot.system.updated.updated import parse_release_notes
|
||||
|
||||
WAIT = int(FPS * 0.5) # Default frames to wait after events
|
||||
# Default frames to wait after events
|
||||
WAIT_LONG = FPS
|
||||
WAIT_SHORT = FPS // 2
|
||||
FAST_CLICK = FPS // 6
|
||||
|
||||
# Direction vectors for drag gestures
|
||||
DIR_LEFT = (-1, 0)
|
||||
DIR_RIGHT = (1, 0)
|
||||
DIR_UP = (0, -1)
|
||||
DIR_DOWN = (0, 1)
|
||||
|
||||
AlertSize = log.SelfdriveState.AlertSize
|
||||
AlertStatus = log.SelfdriveState.AlertStatus
|
||||
@@ -56,49 +68,92 @@ class Script:
|
||||
"""Add a delay for the given number of frames followed by an empty event."""
|
||||
self.add(ScriptEvent(), before=frames)
|
||||
|
||||
def setup(self, fn: Callable, wait_after: int = WAIT) -> None:
|
||||
def setup(self, fn: Callable, wait_after: int = WAIT_SHORT) -> None:
|
||||
"""Add a setup function to be called immediately followed by a delay of the given number of frames."""
|
||||
self.add(ScriptEvent(setup=fn), after=wait_after)
|
||||
|
||||
def set_send(self, fn: Callable, wait_after: int = WAIT) -> None:
|
||||
def set_send(self, fn: Callable, wait_after: int = WAIT_SHORT) -> None:
|
||||
"""Set a new persistent send function to be called every frame."""
|
||||
self.add(ScriptEvent(send_fn=fn), after=wait_after)
|
||||
|
||||
# TODO: Also add more complex gestures, like swipe or drag
|
||||
def click(self, x: int, y: int, wait_after: int = WAIT, wait_between: int = 2) -> None:
|
||||
def click(self, x: int, y: int, wait_after: int = WAIT_SHORT, wait_between: int = 2) -> None:
|
||||
"""Add a click event to the script for the given position and specify frames to wait between mouse events or after the click."""
|
||||
# NOTE: By default we wait a couple frames between mouse events so pressed states will be rendered
|
||||
from openpilot.system.ui.lib.application import MouseEvent, MousePos
|
||||
|
||||
# TODO: Add support for long press (left_down=True)
|
||||
mouse_down = MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=True, left_released=False, left_down=False, t=self.get_frame_time())
|
||||
self.add(ScriptEvent(mouse_events=[mouse_down]), after=wait_between)
|
||||
mouse_up = MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=True, left_down=False, t=self.get_frame_time())
|
||||
self.add(ScriptEvent(mouse_events=[mouse_up]), after=wait_after)
|
||||
|
||||
def drag(self, start_x: int, start_y: int, direction: tuple[int, int], distance: int, duration_frames: int, wait_after: int = WAIT_LONG) -> None:
|
||||
"""Add a drag gesture to the script from start position in the specified direction by the given distance over the given number of frames."""
|
||||
from openpilot.system.ui.lib.application import MouseEvent, MousePos
|
||||
|
||||
# Calculate delta and end position based on direction and distance
|
||||
delta_x, delta_y = direction[0] * distance, direction[1] * distance
|
||||
end_x, end_y = start_x + delta_x, start_y + delta_y
|
||||
|
||||
# Mouse down at start
|
||||
mouse_down = MouseEvent(pos=MousePos(start_x, start_y), slot=0, left_pressed=True, left_released=False, left_down=True, t=self.get_frame_time())
|
||||
self.add(ScriptEvent(mouse_events=[mouse_down]), after=1)
|
||||
|
||||
# Interpolate positions over duration_frames
|
||||
for i in range(1, duration_frames):
|
||||
t = i / duration_frames
|
||||
x, y = int(start_x + delta_x * t), int(start_y + delta_y * t)
|
||||
mouse_move = MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=False, left_down=True, t=self.get_frame_time())
|
||||
self.add(ScriptEvent(mouse_events=[mouse_move]), after=1)
|
||||
|
||||
# Mouse up at end
|
||||
mouse_up = MouseEvent(pos=MousePos(end_x, end_y), slot=0, left_pressed=False, left_released=True, left_down=False, t=self.get_frame_time())
|
||||
self.add(ScriptEvent(mouse_events=[mouse_up]), after=wait_after)
|
||||
|
||||
|
||||
# --- Setup functions ---
|
||||
|
||||
def put_update_params(params: Params | None = None) -> None:
|
||||
if params is None:
|
||||
params = Params()
|
||||
params.put("UpdaterCurrentReleaseNotes", parse_release_notes(BASEDIR))
|
||||
params.put("UpdaterNewReleaseNotes", parse_release_notes(BASEDIR))
|
||||
params.put("UpdaterTargetBranch", BRANCH_NAME)
|
||||
|
||||
def set_prime_state(prime_type: PrimeType) -> None:
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
ui_state.prime_state.set_type(prime_type)
|
||||
|
||||
|
||||
def setup_offroad_alerts() -> None:
|
||||
put_update_params(Params())
|
||||
set_offroad_alert("Offroad_TemperatureTooHigh", True, extra_text='99C')
|
||||
set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text='longitudinal')
|
||||
set_offroad_alert("Offroad_IsTakingSnapshot", True)
|
||||
|
||||
|
||||
def setup_update_available() -> None:
|
||||
def setup_update_available(available: bool = True) -> None:
|
||||
params = Params()
|
||||
params.put_bool("UpdateAvailable", True)
|
||||
params.put_bool("UpdateAvailable", available)
|
||||
params.put("UpdaterAvailableBranches", ",".join(["test-branch", "test-branch-2", BRANCH_NAME]))
|
||||
if available:
|
||||
params.put("UpdaterNewDescription", f"0.10.2 / {BRANCH_NAME} / 0a1b2c3 / Jan 01")
|
||||
put_update_params(params)
|
||||
params.put("UpdaterNewReleaseNotes", parse_release_notes(BASEDIR))
|
||||
params.put("UpdaterTargetBranch", BRANCH_NAME)
|
||||
else:
|
||||
params.remove("UpdaterNewDescription")
|
||||
params.remove("UpdaterNewReleaseNotes")
|
||||
params.remove("UpdaterTargetBranch")
|
||||
|
||||
|
||||
def setup_calibration_params() -> None:
|
||||
params = Params()
|
||||
# live calibration
|
||||
calib = messaging.new_message('liveCalibration')
|
||||
calib.liveCalibration.calStatus = log.LiveCalibrationData.Status.calibrated
|
||||
calib.liveCalibration.rpyCalib = [0.0, math.radians(2.5), math.radians(-1.2)]
|
||||
params.put("CalibrationParams", calib.to_bytes())
|
||||
# live delay
|
||||
delay = messaging.new_message('liveDelay')
|
||||
delay.liveDelay.calPerc = 75
|
||||
params.put("LiveDelay", delay.to_bytes())
|
||||
# live torque parameters
|
||||
torque = messaging.new_message('liveTorqueParameters')
|
||||
torque.liveTorqueParameters.useParams = True
|
||||
torque.liveTorqueParameters.calPerc = 60
|
||||
params.put("LiveTorqueParameters", torque.to_bytes())
|
||||
|
||||
|
||||
def setup_developer_params() -> None:
|
||||
@@ -132,7 +187,6 @@ def make_network_state_setup(pm: PubMaster, network_type) -> Callable:
|
||||
|
||||
def make_alert_setup(pm: PubMaster, size, text1, text2, status) -> Callable:
|
||||
def _send() -> None:
|
||||
send_onroad(pm)
|
||||
alert = messaging.new_message('selfdriveState')
|
||||
ss = alert.selfdriveState
|
||||
ss.alertSize = size
|
||||
@@ -143,18 +197,181 @@ def make_alert_setup(pm: PubMaster, size, text1, text2, status) -> Callable:
|
||||
return _send
|
||||
|
||||
|
||||
def test_onroad_alerts(script: Script, pm: PubMaster) -> None:
|
||||
"""Go through various alert types and sizes and add them to the script to test alert rendering.
|
||||
Each alert is sent as a separate event with a delay in between."""
|
||||
# Small alert (normal)
|
||||
script.set_send(make_alert_setup(pm, AlertSize.small, "Small Alert", "This is a small alert", AlertStatus.normal))
|
||||
# Medium alert (userPrompt)
|
||||
script.set_send(make_alert_setup(pm, AlertSize.mid, "Medium Alert", "This is a medium alert", AlertStatus.userPrompt))
|
||||
# Full alert (critical)
|
||||
script.set_send(make_alert_setup(pm, AlertSize.full, "DISENGAGE IMMEDIATELY", "Driver Distracted", AlertStatus.critical))
|
||||
# Full alert multiline
|
||||
script.set_send(make_alert_setup(pm, AlertSize.full, "Reverse\nGear", "", AlertStatus.normal))
|
||||
# Full alert long text
|
||||
script.set_send(make_alert_setup(pm, AlertSize.full, "TAKE CONTROL IMMEDIATELY", "Calibration Invalid: Remount Device & Recalibrate", AlertStatus.userPrompt))
|
||||
|
||||
|
||||
# --- Script builders ---
|
||||
|
||||
def build_mici_script(pm: PubMaster, main_layout, script: Script) -> None:
|
||||
"""Build the replay script for the mici layout."""
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
|
||||
center = (gui_app.width // 2, gui_app.height // 2)
|
||||
width, height = gui_app.width, gui_app.height
|
||||
center = (width // 2, height // 2)
|
||||
right = (width * 4 // 5, height // 2)
|
||||
left = (width // 5, height // 2)
|
||||
top = (width // 2, height // 10)
|
||||
bottom = (width // 2, height * 9 // 10)
|
||||
|
||||
DURATION = 5
|
||||
SWIPE_WAIT = FPS * 3 // 4
|
||||
|
||||
def click(times: int = 1, wait_after: int = WAIT_SHORT) -> None:
|
||||
"""Click at the center of the screen the given number of times with optional delay after."""
|
||||
for _ in range(times):
|
||||
script.click(*center, wait_after=wait_after)
|
||||
|
||||
def press(x: int, y: int, duration_frames: int = DURATION, wait_after: int = WAIT_SHORT) -> None:
|
||||
"""Perform a drag with no movement to simulate a left_down mouse event at the given position for the specified duration and delay after."""
|
||||
script.drag(x, y, (0, 0), 0, duration_frames, wait_after=wait_after)
|
||||
|
||||
def swipe_left(distance: int = right[0] - left[0], duration_frames: int = DURATION, wait_after: int = SWIPE_WAIT) -> None:
|
||||
"""Drag from right edge to left (scroll right / slide confirmation)."""
|
||||
script.drag(*right, DIR_LEFT, distance, duration_frames, wait_after)
|
||||
|
||||
def swipe_right(distance: int = right[0] - left[0], duration_frames: int = DURATION, wait_after: int = SWIPE_WAIT) -> None:
|
||||
"""Drag from left edge to right (scroll left)."""
|
||||
script.drag(*left, DIR_RIGHT, distance, duration_frames, wait_after)
|
||||
|
||||
def swipe_down(distance: int = bottom[1] - top[1], duration_frames: int = DURATION, wait_after: int = SWIPE_WAIT) -> None:
|
||||
"""Drag from top edge to bottom (scroll up / go back)."""
|
||||
script.drag(*top, DIR_DOWN, distance, duration_frames, wait_after)
|
||||
|
||||
def swipe_up(distance: int = bottom[1] - top[1], duration_frames: int = DURATION, wait_after: int = SWIPE_WAIT) -> None:
|
||||
"""Drag from bottom edge to top (scroll down)."""
|
||||
script.drag(*bottom, DIR_UP, distance, duration_frames, wait_after)
|
||||
|
||||
ActionFn = Callable[[], None] | None
|
||||
Cases = list[ActionFn]
|
||||
|
||||
def run_actions(*actions: ActionFn, after_each: ActionFn = None) -> None:
|
||||
"""Helper function to run a sequence of actions in order for interaction tests, calling after_each callback after each action if provided."""
|
||||
for action in actions:
|
||||
if action is not None:
|
||||
action()
|
||||
if after_each is not None:
|
||||
after_each()
|
||||
|
||||
def explore_setting(*actions: ActionFn) -> None:
|
||||
"""Helper function to open a settings item, run the given actions, and go back."""
|
||||
run_actions(click, *actions, swipe_down) # open, interact, go back
|
||||
|
||||
def scroll_through_cases(cases: Cases) -> None:
|
||||
"""Helper function to explore a panel by calling the interaction callbacks for each item/page before swiping to the next one."""
|
||||
run_actions(*cases, after_each=lambda: swipe_left(210, 10)) # swipe to roughly the center of the next toggle after each case
|
||||
|
||||
def interact_keyboard() -> None:
|
||||
"""Interact with the keyboard in various ways to test different actions and states.
|
||||
Assumes it's a password keyboard with 8 characters required. Closes by pressing confirm at the end."""
|
||||
KEY = (250, 160) # key in the middle of the keyboard ('G')
|
||||
SHIFT = (50, 210)
|
||||
NUMBERS = (480, 210)
|
||||
SPACE = (500, 160)
|
||||
BACKSPACE = (490, 30)
|
||||
CONFIRM = (50, 30)
|
||||
# Begin interactions
|
||||
press(*CONFIRM, wait_after=FAST_CLICK) # confirm while disabled should do nothing
|
||||
swipe_left(duration_frames=FPS // 2) # swipe to type
|
||||
swipe_up(duration_frames=FPS // 2) # swipe out of keyboard (nothing typed)
|
||||
# press various keys to test different states:
|
||||
for key in [
|
||||
SHIFT, KEY, KEY, SHIFT, SHIFT, KEY, KEY, # test casing (upper, lower, caps lock)
|
||||
SPACE, SPACE, BACKSPACE, BACKSPACE, # test multiple space and backspace
|
||||
NUMBERS, KEY, center, SHIFT, KEY # test numbers and symbols
|
||||
]:
|
||||
press(*key, wait_after=FAST_CLICK)
|
||||
# press confirm to close
|
||||
script.wait(WAIT_SHORT) # wait for confirm to enable
|
||||
press(*CONFIRM)
|
||||
|
||||
toggle_cases: Cases = [
|
||||
lambda: click(times=3, wait_after=FAST_CLICK), # first toggle is personality, which has 3 states
|
||||
None, None, None, None, None, None, # skip other toggles to save time
|
||||
lambda: click(times=2, wait_after=FAST_CLICK), # test final toggle (enable openpilot)
|
||||
]
|
||||
|
||||
network_cases: Cases = [
|
||||
explore_setting, # select wifi (just open and close)
|
||||
None, None,
|
||||
lambda: run_actions(click, interact_keyboard), # tether password keyboard
|
||||
]
|
||||
|
||||
device_cases: Cases = [
|
||||
None,
|
||||
click, # update
|
||||
explore_setting, # pairing (just open and close)
|
||||
lambda: explore_setting(
|
||||
# training guide
|
||||
lambda: swipe_left(width * 2), click, # first page, click next
|
||||
lambda: swipe_left(width * 2), swipe_down # second page, go back (TODO: make driver cam preview work)
|
||||
),
|
||||
None, # TODO: preview driver camera; enabling this causes MultiplePublishersError later in onroad alert tests
|
||||
lambda: explore_setting(swipe_left), # terms & conditions (swipe to view QR code)
|
||||
lambda: explore_setting(lambda: swipe_up(height * 3), lambda: swipe_down(height * 3)), # regulatory info
|
||||
lambda: run_actions(click, lambda: swipe_left(width)), # reset calibration confirm (goes back automatically)
|
||||
lambda: explore_setting(lambda: swipe_left(width)), # uninstall
|
||||
lambda: run_actions(
|
||||
lambda: explore_setting(lambda: swipe_left(width)), # reboot
|
||||
lambda: script.click(430, 120), lambda: swipe_left(width), swipe_down, # shutdown
|
||||
),
|
||||
]
|
||||
|
||||
developer_cases: Cases = [
|
||||
lambda: click(times=2, wait_after=FAST_CLICK), # toggle ssh mode
|
||||
explore_setting, # SSH keys keyboard (just open and close)
|
||||
None, # joystick mode
|
||||
lambda: click(wait_after=FAST_CLICK), # longitudinal maneuver mode (disabled; should do nothing)
|
||||
lambda: click(times=2, wait_after=FAST_CLICK), # toggle UI debug mode
|
||||
]
|
||||
|
||||
settings_cases: Cases = [
|
||||
lambda: scroll_through_cases(toggle_cases),
|
||||
lambda: scroll_through_cases(network_cases),
|
||||
lambda: scroll_through_cases(device_cases),
|
||||
lambda: script.wait(WAIT_SHORT), # pairing
|
||||
lambda: run_actions(lambda: swipe_up(height * 3), lambda: swipe_down(height * 3)), # firehose (scroll down and back up)
|
||||
lambda: scroll_through_cases(developer_cases),
|
||||
]
|
||||
|
||||
# === Homescreen === #
|
||||
script.wait(WAIT_SHORT)
|
||||
swipe_left(width, wait_after=WAIT_SHORT) # onroad screen
|
||||
swipe_right(width, wait_after=WAIT_SHORT) # back to home
|
||||
|
||||
# === Offroad Alerts ===
|
||||
def setup_offroad_alerts_and_refresh() -> None:
|
||||
"""Setup function to trigger offroad alerts and force a refresh on the alerts layout."""
|
||||
setup_offroad_alerts()
|
||||
main_layout._alerts_layout.refresh()
|
||||
|
||||
swipe_right(width, wait_after=WAIT_SHORT) # open alerts
|
||||
script.setup(setup_offroad_alerts_and_refresh) # show alerts
|
||||
swipe_up(height) # scroll alerts
|
||||
swipe_left(width, wait_after=WAIT_SHORT) # close alerts
|
||||
|
||||
# === Settings === #
|
||||
click() # open settings
|
||||
scroll_through_cases([lambda case=case: explore_setting(case) for case in settings_cases]) # explore settings
|
||||
swipe_down() # back to home
|
||||
|
||||
# === Onroad ===
|
||||
script.set_send(lambda: send_onroad(pm))
|
||||
swipe_left(width, wait_after=WAIT_SHORT) # onroad screen
|
||||
test_onroad_alerts(script, pm)
|
||||
swipe_right(width) # back to home
|
||||
|
||||
# TODO: Explore more
|
||||
script.wait(FPS)
|
||||
script.click(*center, FPS) # Open settings
|
||||
script.click(*center, FPS) # Open toggles
|
||||
script.end()
|
||||
|
||||
|
||||
@@ -171,34 +388,104 @@ def build_tizi_script(pm: PubMaster, main_layout, script: Script) -> None:
|
||||
|
||||
return setup
|
||||
|
||||
def add_prime_state_setup(prime_type: PrimeType) -> None:
|
||||
script.set_send(lambda: set_prime_state(prime_type))
|
||||
|
||||
def do_onboarding() -> None:
|
||||
"""Click through the training guide and close."""
|
||||
from openpilot.selfdrive.ui.layouts.onboarding import STEP_RECTS
|
||||
step = 0
|
||||
for step_rect in STEP_RECTS:
|
||||
if step < len(STEP_RECTS) - 1:
|
||||
script.click(int(step_rect.x), int(step_rect.y), wait_after=FAST_CLICK)
|
||||
else:
|
||||
script.click(950, 900) # On the last step, click Finish instead of restart
|
||||
step += 1
|
||||
|
||||
def type_keyboard() -> None:
|
||||
"""Types 8 characters using the big keyboard to test different layouts and interactions."""
|
||||
KEY = (150, 430) # e.g. 'Q' key
|
||||
SHIFT = (150, 750) # also symbols key in number mode
|
||||
NUMBERS = (150, 950)
|
||||
SPACE = (1060, 950)
|
||||
BACKSPACE = (2000, 780)
|
||||
for key in [
|
||||
SHIFT, KEY, KEY, SHIFT, SHIFT, KEY, KEY, # test casing (upper, lower, caps lock)
|
||||
SPACE, SPACE, BACKSPACE, BACKSPACE, # test multiple space and backspace
|
||||
NUMBERS, KEY, KEY, SHIFT, KEY, KEY # test numbers and symbols
|
||||
]:
|
||||
script.click(*key, wait_after=FAST_CLICK)
|
||||
|
||||
# TODO: Better way of organizing the events
|
||||
|
||||
# === Homescreen ===
|
||||
script.set_send(make_network_state_setup(pm, log.DeviceState.NetworkType.wifi))
|
||||
|
||||
# === Offroad Alerts (auto-transitions via HomeLayout refresh) ===
|
||||
script.setup(make_home_refresh_setup(setup_offroad_alerts))
|
||||
# Go through different prime state layouts
|
||||
add_prime_state_setup(PrimeType.LITE)
|
||||
add_prime_state_setup(PrimeType.NONE)
|
||||
add_prime_state_setup(PrimeType.UNPAIRED)
|
||||
|
||||
# === Update Available (auto-transitions via HomeLayout refresh) ===
|
||||
script.setup(make_home_refresh_setup(setup_update_available))
|
||||
|
||||
# === Settings - Device (click sidebar settings button) ===
|
||||
# === Offroad Alerts (auto-transitions via HomeLayout refresh, overrides update) ===
|
||||
script.setup(make_home_refresh_setup(setup_offroad_alerts))
|
||||
script.click(620, 950) # close alerts
|
||||
|
||||
# === Settings (click sidebar settings button) ===
|
||||
script.click(150, 90)
|
||||
script.click(1985, 790) # reset calibration confirmation
|
||||
script.click(650, 750) # cancel
|
||||
|
||||
# === Settings - Device ===
|
||||
# pair device
|
||||
script.click(2000, 450) # pair device
|
||||
script.click(110, 110) # close pairing dialog
|
||||
add_prime_state_setup(PrimeType.NONE) # changed from unpaired to hide pair device button
|
||||
# calibration
|
||||
script.setup(setup_calibration_params, wait_after=0)
|
||||
script.click(1000, 620) # expand calibration description
|
||||
script.click(2000, 620) # reset calibration confirmation
|
||||
script.click(1500, 750) # confirm reset
|
||||
script.click(1000, 620) # collapse calibration description
|
||||
# training guide
|
||||
script.click(2000, 800) # open training guide
|
||||
do_onboarding()
|
||||
# regulatory info
|
||||
script.click(2000, 970) # regulatory button
|
||||
script.click(2000, 970) # OK
|
||||
|
||||
# === Settings - Network ===
|
||||
script.click(278, 450)
|
||||
# TODO: mock networks
|
||||
script.click(1880, 100) # advanced network settings
|
||||
script.click(630, 80) # back
|
||||
|
||||
# Keyboard (tethering password)
|
||||
script.click(2000, 420, wait_after=FAST_CLICK) # open tether password keyboard
|
||||
script.click(2000, 950, wait_after=FAST_CLICK) # click confirm (disabled, should not close)
|
||||
script.click(2000, 115) # cancel (close without typing)
|
||||
script.click(2000, 420, wait_after=FAST_CLICK) # open keyboard again
|
||||
type_keyboard() # test various keyboard layouts and interactions
|
||||
script.click(2050, 250, wait_after=FAST_CLICK) # toggle show/hide password
|
||||
script.click(2000, 950) # confirm (close keyboard)
|
||||
|
||||
script.click(630, 80) # back from advanced network
|
||||
|
||||
# === Settings - Toggles ===
|
||||
script.click(278, 600)
|
||||
script.click(1200, 280) # experimental mode description
|
||||
script.click(1200, 280) # expand experimental mode description
|
||||
|
||||
# === Settings - Software ===
|
||||
script.setup(put_update_params, wait_after=0)
|
||||
script.click(278, 720)
|
||||
script.setup(lambda: setup_update_available(False), wait_after=0) # start with no update available
|
||||
script.click(278, 720) # software
|
||||
for _ in range(2):
|
||||
script.click(720, 120) # toggle current release notes
|
||||
script.setup(setup_update_available) # set update available
|
||||
for _ in range(2):
|
||||
script.click(720, 450) # toggle new release notes
|
||||
script.click(2000, 630) # open select branch dialog
|
||||
script.click(1000, 300) # select 1st option
|
||||
script.click(1600, 900) # confirm selection
|
||||
script.click(2000, 800) # uninstall
|
||||
script.click(650, 750) # cancel uninstall
|
||||
|
||||
# === Settings - Firehose ===
|
||||
script.click(278, 845)
|
||||
@@ -206,31 +493,18 @@ def build_tizi_script(pm: PubMaster, main_layout, script: Script) -> None:
|
||||
# === Settings - Developer (set CarParamsPersistent first) ===
|
||||
script.setup(setup_developer_params, wait_after=0)
|
||||
script.click(278, 950)
|
||||
script.click(1930, 470) # SSH keys (keyboard)
|
||||
script.click(1930, 115) # click cancel on keyboard
|
||||
script.click(2000, 960) # toggle alpha long
|
||||
script.click(1500, 875) # confirm
|
||||
|
||||
# === Keyboard modal (SSH keys button in developer panel) ===
|
||||
script.click(1930, 470) # click SSH keys
|
||||
script.click(1930, 115) # click cancel on keyboard
|
||||
|
||||
# === Close settings ===
|
||||
script.click(250, 160)
|
||||
|
||||
# === Onroad ===
|
||||
script.set_send(lambda: send_onroad(pm))
|
||||
script.click(1000, 500) # click onroad to toggle sidebar
|
||||
|
||||
# === Onroad alerts ===
|
||||
# Small alert (normal)
|
||||
script.set_send(make_alert_setup(pm, AlertSize.small, "Small Alert", "This is a small alert", AlertStatus.normal))
|
||||
# Medium alert (userPrompt)
|
||||
script.set_send(make_alert_setup(pm, AlertSize.mid, "Medium Alert", "This is a medium alert", AlertStatus.userPrompt))
|
||||
# Full alert (critical)
|
||||
script.set_send(make_alert_setup(pm, AlertSize.full, "DISENGAGE IMMEDIATELY", "Driver Distracted", AlertStatus.critical))
|
||||
# Full alert multiline
|
||||
script.set_send(make_alert_setup(pm, AlertSize.full, "Reverse\nGear", "", AlertStatus.normal))
|
||||
# Full alert long text
|
||||
script.set_send(make_alert_setup(pm, AlertSize.full, "TAKE CONTROL IMMEDIATELY", "Calibration Invalid: Remount Device & Recalibrate", AlertStatus.userPrompt))
|
||||
test_onroad_alerts(script, pm)
|
||||
|
||||
# End
|
||||
script.end()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user