mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-04-06 16:53:58 +08:00
Merge branch 'master' into hyundai-radar-tracks
This commit is contained in:
@@ -34,10 +34,10 @@ jobs:
|
||||
echo "tinygrad_ref=$ref" >> $GITHUB_OUTPUT
|
||||
echo "tinygrad_ref is $ref"
|
||||
|
||||
- name: Checkout docs repo (sunnypilot-docs, gh-pages)
|
||||
- name: Checkout docs repo (sunnypilot-models, gh-pages)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: sunnypilot/sunnypilot-docs
|
||||
repository: sunnypilot/sunnypilot-models
|
||||
ref: gh-pages
|
||||
path: docs
|
||||
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
- name: Checkout docs repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: sunnypilot/sunnypilot-docs
|
||||
repository: sunnypilot/sunnypilot-models
|
||||
ref: gh-pages
|
||||
path: docs
|
||||
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
|
||||
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
- name: Checkout docs repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: sunnypilot/sunnypilot-docs
|
||||
repository: sunnypilot/sunnypilot-models
|
||||
ref: gh-pages
|
||||
path: docs
|
||||
ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }}
|
||||
|
||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
running-workflow-name: 'build __nightly'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
check-regexp: ^((?!.*(build prebuilt|create badges).*).)*$
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
fetch-depth: 0
|
||||
|
||||
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
|
||||
|
||||
39
.gitignore
vendored
39
.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,51 +58,36 @@ 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
|
||||
TASK.md
|
||||
CLAUDE.md
|
||||
SKILL.md
|
||||
|
||||
### JetBrains ###
|
||||
!.idea/customTargets.xml
|
||||
|
||||
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)
|
||||
========================
|
||||
|
||||
93
SConstruct
93
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,24 +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 openssl3
|
||||
import python3_dev
|
||||
import zeromq
|
||||
import zstd
|
||||
pkgs = [bzip2, capnproto, eigen, ffmpeg_pkg, libjpeg, libyuv, ncurses, openssl3, 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={
|
||||
@@ -108,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"]
|
||||
@@ -127,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:
|
||||
@@ -177,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")
|
||||
|
||||
@@ -211,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'])
|
||||
|
||||
@@ -237,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
|
||||
|
||||
@@ -172,6 +172,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"OnroadScreenOffBrightness", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"OnroadScreenOffBrightnessMigrated", {PERSISTENT | BACKUP, STRING, "0.0"}},
|
||||
{"OnroadScreenOffTimer", {PERSISTENT | BACKUP, INT, "15"}},
|
||||
{"OnroadScreenOffTimerMigrated", {PERSISTENT | BACKUP, STRING, "0.0"}},
|
||||
{"OnroadUploads", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
@@ -271,7 +272,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"EnforceTorqueControl", {PERSISTENT | BACKUP, BOOL}},
|
||||
{"LiveTorqueParamsToggle", {PERSISTENT | BACKUP , BOOL}},
|
||||
{"LiveTorqueParamsRelaxedToggle", {PERSISTENT | BACKUP , BOOL}},
|
||||
{"TorqueControlTune", {PERSISTENT | BACKUP, FLOAT}},
|
||||
{"TorqueControlTune", {PERSISTENT | BACKUP, FLOAT, "0.0"}},
|
||||
{"TorqueParamsOverrideEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"TorqueParamsOverrideFriction", {PERSISTENT | BACKUP, FLOAT, "0.1"}},
|
||||
{"TorqueParamsOverrideLatAccelFactor", {PERSISTENT | BACKUP, FLOAT, "2.5"}},
|
||||
|
||||
@@ -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"
|
||||
|
||||
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",
|
||||
"openssl3 @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=openssl3",
|
||||
"python3-dev @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=python3-dev",
|
||||
"zstd @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zstd",
|
||||
"ncurses @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=ncurses",
|
||||
"zeromq @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=zeromq",
|
||||
"git-lfs @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=git-lfs",
|
||||
"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",
|
||||
@@ -76,6 +76,7 @@ dependencies = [
|
||||
"raylib > 5.5.0.3",
|
||||
"qrcode",
|
||||
"jeepney",
|
||||
"pillow",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
@@ -103,12 +104,10 @@ testing = [
|
||||
dev = [
|
||||
"matplotlib",
|
||||
"opencv-python-headless",
|
||||
"gcc-arm-none-eabi @ git+https://github.com/commaai/dependencies.git@releases#subdirectory=gcc-arm-none-eabi",
|
||||
]
|
||||
|
||||
tools = [
|
||||
"metadrive-simulator @ git+https://github.com/commaai/metadrive.git@minimal ; (platform_machine != 'aarch64')",
|
||||
"dearpygui>=2.1.0; (sys_platform != 'linux' or platform_machine != 'aarch64')", # not vended for linux aarch64
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -207,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"
|
||||
@@ -250,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"
|
||||
|
||||
@@ -12,12 +12,13 @@ from openpilot.common.basedir import BASEDIR
|
||||
|
||||
|
||||
DIRS = ['cereal', 'openpilot']
|
||||
EXTS = ['.png', '.py', '.ttf', '.capnp', '.json', '.fnt', '.mo']
|
||||
EXTS = ['.png', '.py', '.ttf', '.capnp', '.json', '.fnt', '.mo', '.po']
|
||||
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/cancel.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/setup/cancel.png
LFS
Normal file
Binary file not shown.
BIN
selfdrive/assets/icons_mici/setup/continue.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/setup/continue.png
LFS
Normal file
Binary file not shown.
BIN
selfdrive/assets/icons_mici/setup/continue_disabled.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/setup/continue_disabled.png
LFS
Normal file
Binary file not shown.
BIN
selfdrive/assets/icons_mici/setup/continue_pressed.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/setup/continue_pressed.png
LFS
Normal file
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.
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
|
||||
|
||||
@@ -32,8 +32,7 @@ def flash_panda(panda_serial: str) -> Panda:
|
||||
raise
|
||||
|
||||
# skip flashing if the detected panda is not supported
|
||||
supported_panda = check_panda_support(panda)
|
||||
if not supported_panda:
|
||||
if panda.get_type() not in Panda.SUPPORTED_DEVICES:
|
||||
cloudlog.warning(f"Panda {panda_serial} is not supported (hw_type: {panda.get_type()}), skipping flash...")
|
||||
return panda
|
||||
|
||||
@@ -69,12 +68,20 @@ def flash_panda(panda_serial: str) -> Panda:
|
||||
return panda
|
||||
|
||||
|
||||
def check_panda_support(panda) -> bool:
|
||||
hw_type = panda.get_type()
|
||||
if hw_type in Panda.SUPPORTED_DEVICES:
|
||||
return True
|
||||
def check_panda_support(panda_serials: list[str]) -> list[str]:
|
||||
spi_serials = set(Panda.spi_list())
|
||||
for serial in panda_serials:
|
||||
if serial in spi_serials:
|
||||
return [serial]
|
||||
|
||||
return False
|
||||
for serial in panda_serials:
|
||||
panda = Panda(serial)
|
||||
is_internal = panda.is_internal()
|
||||
panda.close()
|
||||
if is_internal:
|
||||
return [serial]
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -126,13 +133,18 @@ def main() -> None:
|
||||
|
||||
cloudlog.info(f"{len(panda_serials)} panda(s) found, connecting - {panda_serials}")
|
||||
|
||||
# custom flasher for xnor's Rivian Longitudinal Upgrade Kit
|
||||
flash_rivian_long(panda_serials)
|
||||
|
||||
# find the internal supported panda (e.g. skip external Black Panda)
|
||||
panda_serials = check_panda_support(panda_serials)
|
||||
if len(panda_serials) == 0:
|
||||
continue
|
||||
|
||||
# Flash the first panda
|
||||
panda_serial = panda_serials[0]
|
||||
panda = flash_panda(panda_serial)
|
||||
|
||||
# flash Rivian longitudinal upgrade panda
|
||||
flash_rivian_long(panda)
|
||||
|
||||
# Ensure internal panda is present if expected
|
||||
if HARDWARE.has_internal_panda() and not panda.is_internal():
|
||||
cloudlog.error("Internal panda is missing, trying again")
|
||||
@@ -143,12 +155,6 @@ def main() -> None:
|
||||
# log panda fw version
|
||||
params.put("PandaSignatures", panda.get_signature())
|
||||
|
||||
# skip health check if the detected panda is not supported
|
||||
supported_panda = check_panda_support(panda)
|
||||
if not supported_panda:
|
||||
cloudlog.warning(f"Panda {panda.get_usb_serial()} is not supported (hw_type: {panda.get_type()}), skipping health check...")
|
||||
continue
|
||||
|
||||
# check health for lost heartbeat
|
||||
health = panda.health()
|
||||
if health["heartbeat_lost"]:
|
||||
|
||||
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,39 +18,38 @@ 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')
|
||||
raylib_env = env.Clone()
|
||||
raylib_env['LIBPATH'] += [f'#third_party/raylib/{arch}/']
|
||||
raylib_env['LINKFLAGS'].append('-Wl,-strip-debug')
|
||||
|
||||
raylib_libs = common + ["raylib"]
|
||||
if arch == "larch64":
|
||||
raylib_libs += ["GLESv2", "EGL", "gbm", "drm"]
|
||||
else:
|
||||
raylib_libs += ["GL"]
|
||||
raylib_libs = common + ["raylib"]
|
||||
if arch == "larch64":
|
||||
raylib_libs += ["GLESv2", "EGL", "gbm", "drm"]
|
||||
else:
|
||||
raylib_libs += ["GL"]
|
||||
|
||||
release = "release3"
|
||||
installers = [
|
||||
("openpilot", release),
|
||||
("openpilot_test", f"{release}-staging"),
|
||||
("openpilot_nightly", "nightly"),
|
||||
("openpilot_internal", "nightly-dev"),
|
||||
]
|
||||
release = "release3"
|
||||
installers = [
|
||||
("openpilot", release),
|
||||
("openpilot_test", f"{release}-staging"),
|
||||
("openpilot_nightly", "nightly"),
|
||||
("openpilot_internal", "nightly-dev"),
|
||||
]
|
||||
|
||||
cont = raylib_env.Command("installer/continue_openpilot.o", "installer/continue_openpilot.sh",
|
||||
cont = raylib_env.Command("installer/continue_openpilot.o", "installer/continue_openpilot.sh",
|
||||
"ld -r -b binary -o $TARGET $SOURCE")
|
||||
inter = raylib_env.Command("installer/inter_ttf.o", "installer/inter-ascii.ttf",
|
||||
"ld -r -b binary -o $TARGET $SOURCE")
|
||||
inter_bold = raylib_env.Command("installer/inter_bold.o", "../assets/fonts/Inter-Bold.ttf",
|
||||
"ld -r -b binary -o $TARGET $SOURCE")
|
||||
inter = raylib_env.Command("installer/inter_ttf.o", "installer/inter-ascii.ttf",
|
||||
"ld -r -b binary -o $TARGET $SOURCE")
|
||||
inter_bold = raylib_env.Command("installer/inter_bold.o", "../assets/fonts/Inter-Bold.ttf",
|
||||
"ld -r -b binary -o $TARGET $SOURCE")
|
||||
inter_light = raylib_env.Command("installer/inter_light.o", "../assets/fonts/Inter-Light.ttf",
|
||||
"ld -r -b binary -o $TARGET $SOURCE")
|
||||
for name, branch in installers:
|
||||
d = {'BRANCH': f"'\"{branch}\"'"}
|
||||
if "internal" in name:
|
||||
d['INTERNAL'] = "1"
|
||||
inter_light = raylib_env.Command("installer/inter_light.o", "../assets/fonts/Inter-Light.ttf",
|
||||
"ld -r -b binary -o $TARGET $SOURCE")
|
||||
for name, branch in installers:
|
||||
d = {'BRANCH': f"'\"{branch}\"'"}
|
||||
if "internal" in name:
|
||||
d['INTERNAL'] = "1"
|
||||
|
||||
obj = raylib_env.Object(f"installer/installers/installer_{name}.o", ["installer/installer.cc"], CPPDEFINES=d)
|
||||
f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter, inter_bold, inter_light], LIBS=raylib_libs)
|
||||
# keep installers small
|
||||
assert f[0].get_size() < 2500*1e3, f[0].get_size()
|
||||
obj = raylib_env.Object(f"installer/installers/installer_{name}.o", ["installer/installer.cc"], CPPDEFINES=d)
|
||||
f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter, inter_bold, inter_light], LIBS=raylib_libs)
|
||||
# keep installers small
|
||||
assert f[0].get_size() < 2500*1e3, f[0].get_size()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -69,7 +69,6 @@ class SoftwareLayout(Widget):
|
||||
|
||||
# Branch switcher
|
||||
self._branch_btn = button_item(lambda: tr("Target Branch"), lambda: tr("SELECT"), callback=self._on_select_branch)
|
||||
self._branch_btn.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
|
||||
self._branch_btn.action_item.set_value(ui_state.params.get("UpdaterTargetBranch") or "")
|
||||
self._branch_dialog: MultiOptionDialog | None = None
|
||||
|
||||
@@ -83,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:
|
||||
|
||||
@@ -56,7 +56,7 @@ class MiciMainLayout(Scroller):
|
||||
gui_app.push_widget(self)
|
||||
|
||||
# Start onboarding if terms or training not completed, make sure to push after self
|
||||
self._onboarding_window = OnboardingWindow()
|
||||
self._onboarding_window = OnboardingWindow(lambda: gui_app.pop_widgets_to(self))
|
||||
if not self._onboarding_window.completed:
|
||||
gui_app.push_widget(self._onboarding_window)
|
||||
|
||||
@@ -82,7 +82,7 @@ class MiciMainLayout(Scroller):
|
||||
|
||||
def _handle_transitions(self):
|
||||
# Don't pop if onboarding
|
||||
if gui_app.get_active_widget() == self._onboarding_window:
|
||||
if gui_app.widget_in_stack(self._onboarding_window):
|
||||
return
|
||||
|
||||
if ui_state.started != self._prev_onroad:
|
||||
@@ -108,7 +108,7 @@ class MiciMainLayout(Scroller):
|
||||
|
||||
def _on_interactive_timeout(self):
|
||||
# Don't pop if onboarding
|
||||
if gui_app.get_active_widget() == self._onboarding_window:
|
||||
if gui_app.widget_in_stack(self._onboarding_window):
|
||||
return
|
||||
|
||||
if ui_state.started:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,32 +1,24 @@
|
||||
from enum import IntEnum
|
||||
|
||||
import weakref
|
||||
import math
|
||||
import numpy as np
|
||||
import qrcode
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.button import SmallButton, SmallCircleIconButton
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.slider import SmallSlider
|
||||
from openpilot.system.ui.mici_setup import TermsHeader, TermsPage as SetupTermsPage
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import BaseDriverCameraDialog
|
||||
from openpilot.system.ui.widgets.button import SmallCircleIconButton
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller, Scroller
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
from openpilot.system.ui.mici_setup import GreyBigButton, BigPillButton
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.version import terms_version, training_version, terms_version_sp
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkOnboarding
|
||||
|
||||
|
||||
class OnboardingState(IntEnum):
|
||||
TERMS = 0
|
||||
ONBOARDING = 1
|
||||
DECLINE = 2
|
||||
SUNNYLINK_CONSENT = 3
|
||||
from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.selfdrive.ui.mici.widgets.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
|
||||
|
||||
|
||||
class DriverCameraSetupDialog(BaseDriverCameraDialog):
|
||||
@@ -60,91 +52,62 @@ class DriverCameraSetupDialog(BaseDriverCameraDialog):
|
||||
rl.end_scissor_mode()
|
||||
|
||||
|
||||
class TrainingGuidePreDMTutorial(SetupTermsPage):
|
||||
def __init__(self, continue_callback):
|
||||
super().__init__(continue_callback, continue_text="continue")
|
||||
self._title_header = TermsHeader("driver monitoring setup", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
|
||||
class TrainingGuidePreDMTutorial(NavScroller):
|
||||
def __init__(self, continue_callback: Callable[[], None]):
|
||||
super().__init__()
|
||||
|
||||
self._dm_label = UnifiedLabel("Next, we'll ensure comma four is mounted properly.\n\nIf it does not have a clear view of the driver, " +
|
||||
"unplug and remount before continuing.", 42,
|
||||
FontWeight.ROMAN)
|
||||
continue_button = BigPillButton("next")
|
||||
continue_button.set_click_callback(continue_callback)
|
||||
|
||||
self._scroller.add_widgets([
|
||||
GreyBigButton("driver monitoring\ncheck", "scroll to continue",
|
||||
gui_app.texture("icons_mici/setup/green_dm.png", 64, 64)),
|
||||
GreyBigButton("", "Next, we'll check if comma four can detect the driver properly."),
|
||||
GreyBigButton("", "sunnypilot uses the cabin camera to check if the driver is distracted."),
|
||||
GreyBigButton("", "If it does not have a clear view of the driver, unplug and remount before continuing."),
|
||||
continue_button,
|
||||
])
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
# Get driver monitoring model ready for next step
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", True)
|
||||
|
||||
@property
|
||||
def _content_height(self):
|
||||
return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset()
|
||||
|
||||
def _render_content(self, scroll_offset):
|
||||
self._title_header.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._rect.y + 16 + scroll_offset,
|
||||
self._title_header.rect.width,
|
||||
self._title_header.rect.height,
|
||||
))
|
||||
|
||||
self._dm_label.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._title_header.rect.y + self._title_header.rect.height + 16,
|
||||
self._rect.width - 32,
|
||||
self._dm_label.get_content_height(int(self._rect.width - 32)),
|
||||
))
|
||||
ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", True)
|
||||
|
||||
|
||||
class DMBadFaceDetected(SetupTermsPage):
|
||||
def __init__(self, continue_callback, back_callback):
|
||||
super().__init__(continue_callback, back_callback, continue_text="power off")
|
||||
self._title_header = TermsHeader("make sure comma four can see your face", gui_app.texture("icons_mici/setup/orange_dm.png", 60, 60))
|
||||
self._dm_label = UnifiedLabel("Re-mount if your face is occluded or driver monitoring has difficulty tracking your face.", 42, FontWeight.ROMAN)
|
||||
class DMBadFaceDetected(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
def _content_height(self):
|
||||
return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset()
|
||||
back_button = BigPillButton("back")
|
||||
back_button.set_click_callback(self.dismiss)
|
||||
|
||||
def _render_content(self, scroll_offset):
|
||||
self._title_header.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._rect.y + 16 + scroll_offset,
|
||||
self._title_header.rect.width,
|
||||
self._title_header.rect.height,
|
||||
))
|
||||
|
||||
self._dm_label.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._title_header.rect.y + self._title_header.rect.height + 16,
|
||||
self._rect.width - 32,
|
||||
self._dm_label.get_content_height(int(self._rect.width - 32)),
|
||||
))
|
||||
self._scroller.add_widgets([
|
||||
GreyBigButton("looking for driver", "make sure comma\nfour can see your face",
|
||||
gui_app.texture("icons_mici/setup/orange_dm.png", 64, 64)),
|
||||
GreyBigButton("", "Remount if your face is blocked, or driver monitoring has difficulty tracking your face."),
|
||||
back_button,
|
||||
])
|
||||
|
||||
|
||||
class TrainingGuideDMTutorial(Widget):
|
||||
class TrainingGuideDMTutorial(NavWidget):
|
||||
PROGRESS_DURATION = 4
|
||||
LOOKING_THRESHOLD_DEG = 30.0
|
||||
|
||||
def __init__(self, continue_callback):
|
||||
def __init__(self, continue_callback: Callable[[], None]):
|
||||
super().__init__()
|
||||
|
||||
self_ref = weakref.ref(self)
|
||||
|
||||
self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 28, 48))
|
||||
self._back_button.set_click_callback(lambda: self_ref() and self_ref()._show_bad_face_page())
|
||||
self._back_button.set_click_callback(lambda: gui_app.push_widget(self._bad_face_page))
|
||||
self._back_button.set_touch_valid_callback(lambda: self.enabled and not self.is_dismissing) # for nav stack
|
||||
self._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 42, 42))
|
||||
self._good_button.set_touch_valid_callback(lambda: self.enabled and not self.is_dismissing) # for nav stack
|
||||
|
||||
# Wrap the continue callback to restore settings
|
||||
def wrapped_continue_callback():
|
||||
device.set_offroad_brightness(None)
|
||||
continue_callback()
|
||||
|
||||
self._good_button.set_click_callback(wrapped_continue_callback)
|
||||
self._good_button.set_click_callback(continue_callback)
|
||||
self._good_button.set_enabled(False)
|
||||
|
||||
self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps)
|
||||
self._dialog = DriverCameraSetupDialog()
|
||||
self._bad_face_page = DMBadFaceDetected(HARDWARE.shutdown, lambda: self_ref() and self_ref()._hide_bad_face_page())
|
||||
self._should_show_bad_face_page = False
|
||||
self._bad_face_page = DMBadFaceDetected()
|
||||
|
||||
# Disable driver monitoring model when device times out for inactivity
|
||||
def inactivity_callback():
|
||||
@@ -152,23 +115,11 @@ class TrainingGuideDMTutorial(Widget):
|
||||
|
||||
device.add_interactive_timeout_callback(inactivity_callback)
|
||||
|
||||
def _show_bad_face_page(self):
|
||||
self._bad_face_page.show_event()
|
||||
self.hide_event()
|
||||
self._should_show_bad_face_page = True
|
||||
|
||||
def _hide_bad_face_page(self):
|
||||
self._bad_face_page.hide_event()
|
||||
self.show_event()
|
||||
self._should_show_bad_face_page = False
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._dialog.show_event()
|
||||
self._progress.x = 0.0
|
||||
|
||||
device.set_offroad_brightness(100)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
if device.awake and not ui_state.params.get_bool("IsDriverViewEnabled"):
|
||||
@@ -188,7 +139,8 @@ class TrainingGuideDMTutorial(Widget):
|
||||
looking_center = False
|
||||
|
||||
# stay at 100% once reached
|
||||
if (dm_state.faceDetected and looking_center) or self._progress.x > 0.99:
|
||||
in_bad_face = gui_app.get_active_widget() == self._bad_face_page
|
||||
if ((dm_state.faceDetected and looking_center) or self._progress.x > 0.99) and not in_bad_face:
|
||||
slow = self._progress.x < 0.25
|
||||
duration = self.PROGRESS_DURATION * 2 if slow else self.PROGRESS_DURATION
|
||||
self._progress.x += 1.0 / (duration * gui_app.target_fps)
|
||||
@@ -199,13 +151,12 @@ class TrainingGuideDMTutorial(Widget):
|
||||
self._good_button.set_enabled(self._progress.x >= 0.999)
|
||||
|
||||
def _render(self, _):
|
||||
if self._should_show_bad_face_page:
|
||||
return self._bad_face_page.render(self._rect)
|
||||
|
||||
self._dialog.render(self._rect)
|
||||
|
||||
rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - 80),
|
||||
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
|
||||
@@ -258,266 +209,229 @@ class TrainingGuideDMTutorial(Widget):
|
||||
))
|
||||
|
||||
# rounded border
|
||||
rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height))
|
||||
rl.draw_rectangle_rounded_lines_ex(self._rect, 0.2 * 1.02, 10, 50, rl.BLACK)
|
||||
rl.end_scissor_mode()
|
||||
|
||||
|
||||
class TrainingGuideRecordFront(SetupTermsPage):
|
||||
def __init__(self, continue_callback):
|
||||
def on_back():
|
||||
ui_state.params.put_bool("RecordFront", False)
|
||||
continue_callback()
|
||||
|
||||
def on_continue():
|
||||
ui_state.params.put_bool("RecordFront", True)
|
||||
continue_callback()
|
||||
|
||||
super().__init__(on_continue, back_callback=on_back, back_text="no", continue_text="yes")
|
||||
self._title_header = TermsHeader("improve driver monitoring", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
|
||||
|
||||
self._dm_label = UnifiedLabel("Do you want to upload driver camera data?", 42,
|
||||
FontWeight.ROMAN)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
# Disable driver monitoring model after last step
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", False)
|
||||
|
||||
@property
|
||||
def _content_height(self):
|
||||
return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset()
|
||||
|
||||
def _render_content(self, scroll_offset):
|
||||
self._title_header.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._rect.y + 16 + scroll_offset,
|
||||
self._title_header.rect.width,
|
||||
self._title_header.rect.height,
|
||||
))
|
||||
|
||||
self._dm_label.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._title_header.rect.y + self._title_header.rect.height + 16,
|
||||
self._rect.width - 32,
|
||||
self._dm_label.get_content_height(int(self._rect.width - 32)),
|
||||
))
|
||||
|
||||
|
||||
class TrainingGuideAttentionNotice(SetupTermsPage):
|
||||
def __init__(self, continue_callback):
|
||||
super().__init__(continue_callback, continue_text="continue")
|
||||
self._title_header = TermsHeader("driver assistance", gui_app.texture("icons_mici/setup/warning.png", 60, 60))
|
||||
self._warning_label = UnifiedLabel("1. sunnypilot is a driver assistance system.\n\n" +
|
||||
"2. You must pay attention at all times.\n\n" +
|
||||
"3. You must be ready to take over at any time.\n\n" +
|
||||
"4. You are fully responsible for driving the car.", 42,
|
||||
FontWeight.ROMAN)
|
||||
|
||||
@property
|
||||
def _content_height(self):
|
||||
return self._warning_label.rect.y + self._warning_label.rect.height - self._scroll_panel.get_offset()
|
||||
|
||||
def _render_content(self, scroll_offset):
|
||||
self._title_header.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._rect.y + 16 + scroll_offset,
|
||||
self._title_header.rect.width,
|
||||
self._title_header.rect.height,
|
||||
))
|
||||
|
||||
self._warning_label.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._title_header.rect.y + self._title_header.rect.height + 16,
|
||||
self._rect.width - 32,
|
||||
self._warning_label.get_content_height(int(self._rect.width - 32)),
|
||||
))
|
||||
|
||||
|
||||
class TrainingGuide(Widget):
|
||||
def __init__(self, completed_callback=None):
|
||||
class TrainingGuideRecordFront(NavScroller):
|
||||
def __init__(self, continue_callback: Callable[[], None]):
|
||||
super().__init__()
|
||||
self._completed_callback = completed_callback
|
||||
self._step = 0
|
||||
|
||||
self_ref = weakref.ref(self)
|
||||
def on_accept():
|
||||
ui_state.params.put_bool_nonblocking("RecordFront", True)
|
||||
continue_callback()
|
||||
|
||||
def on_continue():
|
||||
if obj := self_ref():
|
||||
obj._advance_step()
|
||||
def on_decline():
|
||||
ui_state.params.put_bool_nonblocking("RecordFront", False)
|
||||
continue_callback()
|
||||
|
||||
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._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?",
|
||||
gui_app.texture("icons_mici/setup/green_dm.png", 64, 64)),
|
||||
GreyBigButton("", "Sharing your data with comma helps improve openpilot and sunnypilot for everyone."),
|
||||
self._accept_button,
|
||||
self._decline_button,
|
||||
])
|
||||
|
||||
|
||||
class TrainingGuideAttentionNotice(Scroller):
|
||||
def __init__(self, continue_callback: Callable[[], None]):
|
||||
super().__init__()
|
||||
|
||||
continue_button = BigPillButton("next")
|
||||
continue_button.set_click_callback(continue_callback)
|
||||
|
||||
self._scroller.add_widgets([
|
||||
GreyBigButton("what is sunnypilot?", "scroll to continue",
|
||||
gui_app.texture("icons_mici/setup/green_info.png", 64, 64)),
|
||||
GreyBigButton("", "1. sunnypilot is a driver assistance system."),
|
||||
GreyBigButton("", "2. You must pay attention at all times."),
|
||||
GreyBigButton("", "3. You must be ready to take over at any time."),
|
||||
GreyBigButton("", "4. You are fully responsible for driving the car."),
|
||||
continue_button,
|
||||
])
|
||||
|
||||
|
||||
class TrainingGuide(NavWidget):
|
||||
def __init__(self, completed_callback: Callable[[], None]):
|
||||
super().__init__()
|
||||
|
||||
self._steps = [
|
||||
TrainingGuideAttentionNotice(continue_callback=on_continue),
|
||||
TrainingGuidePreDMTutorial(continue_callback=on_continue),
|
||||
TrainingGuideDMTutorial(continue_callback=on_continue),
|
||||
TrainingGuideRecordFront(continue_callback=on_continue),
|
||||
TrainingGuideAttentionNotice(continue_callback=lambda: gui_app.push_widget(self._steps[1])),
|
||||
TrainingGuidePreDMTutorial(continue_callback=lambda: gui_app.push_widget(self._steps[2])),
|
||||
TrainingGuideDMTutorial(continue_callback=lambda: gui_app.push_widget(self._steps[3])),
|
||||
TrainingGuideRecordFront(continue_callback=completed_callback),
|
||||
]
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
device.set_override_interactive_timeout(300)
|
||||
self._child(self._steps[0])
|
||||
self._steps[0].set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
device.set_override_interactive_timeout(None)
|
||||
def _render(self, _):
|
||||
self._steps[0].render(self._rect)
|
||||
|
||||
def _advance_step(self):
|
||||
if self._step < len(self._steps) - 1:
|
||||
self._step += 1
|
||||
self._steps[self._step].show_event()
|
||||
else:
|
||||
self._step = 0
|
||||
if self._completed_callback:
|
||||
self._completed_callback()
|
||||
|
||||
class QRCodeWidget(Widget):
|
||||
def __init__(self, url: str, size: int = 170):
|
||||
super().__init__()
|
||||
self.set_rect(rl.Rectangle(0, 0, size, size))
|
||||
self._size = size
|
||||
self._qr_texture: rl.Texture | None = None
|
||||
self._generate_qr(url)
|
||||
|
||||
def _generate_qr(self, url: str):
|
||||
qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=0)
|
||||
qr.add_data(url)
|
||||
qr.make(fit=True)
|
||||
|
||||
pil_img = qr.make_image(fill_color="white", back_color="black").convert('RGBA')
|
||||
img_array = np.array(pil_img, dtype=np.uint8)
|
||||
|
||||
rl_image = rl.Image()
|
||||
rl_image.data = rl.ffi.cast("void *", img_array.ctypes.data)
|
||||
rl_image.width = pil_img.width
|
||||
rl_image.height = pil_img.height
|
||||
rl_image.mipmaps = 1
|
||||
rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
|
||||
|
||||
self._qr_texture = rl.load_texture_from_image(rl_image)
|
||||
|
||||
def _render(self, _):
|
||||
if self._qr_texture:
|
||||
scale = self._size / self._qr_texture.height
|
||||
rl.draw_texture_ex(self._qr_texture, rl.Vector2(round(self._rect.x), round(self._rect.y)), 0.0, scale, rl.WHITE)
|
||||
|
||||
def __del__(self):
|
||||
if self._qr_texture and self._qr_texture.id != 0:
|
||||
rl.unload_texture(self._qr_texture)
|
||||
|
||||
|
||||
class TermsPage(Scroller):
|
||||
def __init__(self, on_accept, on_decline):
|
||||
super().__init__()
|
||||
|
||||
self._accept_button = BigConfirmationCircleButton("accept\nterms", gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 64, 64), on_accept)
|
||||
self._decline_button = BigConfirmationCircleButton("decline &\nuninstall", gui_app.texture("icons_mici/setup/cancel.png", 64, 64), on_decline,
|
||||
red=True, exit_on_confirm=False)
|
||||
|
||||
self._terms_header = GreyBigButton("terms of\nservice", "scroll to continue",
|
||||
gui_app.texture("icons_mici/setup/green_info.png", 64, 64))
|
||||
self._must_accept_card = GreyBigButton("", "You must accept the Terms of Service to use sunnypilot.")
|
||||
|
||||
self._scroller.add_widgets([
|
||||
self._terms_header,
|
||||
GreyBigButton("swipe for QR code", "or go to https://sunnypilot.ai/terms",
|
||||
gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 64, 56, flip_x=True)),
|
||||
QRCodeWidget("https://sunnypilot.ai/terms"),
|
||||
self._must_accept_card,
|
||||
self._accept_button,
|
||||
self._decline_button,
|
||||
])
|
||||
|
||||
def _render(self, _):
|
||||
rl.draw_rectangle_rec(self._rect, rl.BLACK)
|
||||
if self._step < len(self._steps):
|
||||
self._steps[self._step].render(self._rect)
|
||||
|
||||
|
||||
class DeclinePage(Widget):
|
||||
def __init__(self, back_callback=None):
|
||||
super().__init__()
|
||||
self._uninstall_slider = SmallSlider("uninstall sunnypilot", self._on_uninstall)
|
||||
|
||||
self._back_button = SmallButton("back")
|
||||
self._back_button.set_click_callback(back_callback)
|
||||
|
||||
self._warning_header = TermsHeader("you must accept the\nterms to use sunnypilot",
|
||||
gui_app.texture("icons_mici/setup/red_warning.png", 66, 60))
|
||||
|
||||
def _on_uninstall(self):
|
||||
ui_state.params.put_bool("DoUninstall", True)
|
||||
gui_app.request_close()
|
||||
|
||||
def _render(self, _):
|
||||
self._warning_header.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._rect.y + 16,
|
||||
self._warning_header.rect.width,
|
||||
self._warning_header.rect.height,
|
||||
))
|
||||
|
||||
self._back_button.set_opacity(1 - self._uninstall_slider.slider_percentage)
|
||||
self._back_button.render(rl.Rectangle(
|
||||
self._rect.x + 8,
|
||||
self._rect.y + self._rect.height - self._back_button.rect.height,
|
||||
self._back_button.rect.width,
|
||||
self._back_button.rect.height,
|
||||
))
|
||||
|
||||
self._uninstall_slider.render(rl.Rectangle(
|
||||
self._rect.x + self._rect.width - self._uninstall_slider.rect.width,
|
||||
self._rect.y + self._rect.height - self._uninstall_slider.rect.height,
|
||||
self._uninstall_slider.rect.width,
|
||||
self._uninstall_slider.rect.height,
|
||||
))
|
||||
|
||||
|
||||
class TermsPage(SetupTermsPage):
|
||||
def __init__(self, on_accept=None, on_decline=None):
|
||||
super().__init__(on_accept, on_decline, "decline")
|
||||
|
||||
info_txt = gui_app.texture("icons_mici/setup/green_info.png", 60, 60)
|
||||
self._title_header = TermsHeader("terms of service", info_txt)
|
||||
|
||||
self._terms_label = UnifiedLabel("You must accept the Terms of Service to use sunnypilot. " +
|
||||
"Read the latest terms at https://sunnypilot.ai/terms before continuing.", 36,
|
||||
FontWeight.ROMAN)
|
||||
|
||||
@property
|
||||
def _content_height(self):
|
||||
return self._terms_label.rect.y + self._terms_label.rect.height - self._scroll_panel.get_offset()
|
||||
|
||||
def _render_content(self, scroll_offset):
|
||||
self._title_header.set_position(self._rect.x + 16, self._rect.y + 12 + scroll_offset)
|
||||
self._title_header.render()
|
||||
|
||||
self._terms_label.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._title_header.rect.y + self._title_header.rect.height + self.ITEM_SPACING,
|
||||
self._rect.width - 100,
|
||||
self._terms_label.get_content_height(int(self._rect.width - 100)),
|
||||
))
|
||||
super()._render(_)
|
||||
|
||||
|
||||
class OnboardingWindow(Widget):
|
||||
def __init__(self):
|
||||
def __init__(self, completed_callback: Callable[[], None]):
|
||||
super().__init__()
|
||||
self._accepted_terms: bool = ui_state.params.get("HasAcceptedTerms") == terms_version
|
||||
self._completed_callback = completed_callback
|
||||
self._accepted_terms: bool = (ui_state.params.get("HasAcceptedTerms") == terms_version and
|
||||
ui_state.params.get("HasAcceptedTermsSP") == terms_version_sp)
|
||||
self._training_done: bool = ui_state.params.get("CompletedTrainingVersion") == training_version
|
||||
self._sunnylink_consent_done: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") in {
|
||||
sunnylink_consent_version, sunnylink_consent_declined
|
||||
}
|
||||
|
||||
self._state = OnboardingState.TERMS if not self._accepted_terms else OnboardingState.ONBOARDING
|
||||
self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
|
||||
self.set_rect(rl.Rectangle(0, 0, 458, gui_app.height))
|
||||
# Windows — all pushed onto nav stack, _terms is always rendered as base layer
|
||||
self._terms = TermsPage(on_accept=self._on_terms_accepted, on_decline=self._on_uninstall)
|
||||
self._terms.set_enabled(lambda: self.enabled) # for nav stack
|
||||
|
||||
self._sunnylink_consent = SunnylinkConsentPage(
|
||||
on_accept=self._on_sunnylink_accepted,
|
||||
on_decline=self._on_sunnylink_declined,
|
||||
)
|
||||
|
||||
# Windows
|
||||
self._terms = TermsPage(on_accept=self._on_terms_accepted, on_decline=self._on_terms_declined)
|
||||
self._training_guide = TrainingGuide(completed_callback=self._on_completed_training)
|
||||
self._decline_page = DeclinePage(back_callback=self._on_decline_back)
|
||||
self._training_guide.set_enabled(lambda: self.enabled) # for nav stack
|
||||
|
||||
# sunnylink consent pages
|
||||
self._accepted_terms = self._accepted_terms and ui_state.params.get("HasAcceptedTermsSP") == terms_version_sp
|
||||
self._sunnylink = SunnylinkOnboarding()
|
||||
if not self._accepted_terms:
|
||||
self._state = OnboardingState.TERMS
|
||||
elif not self._sunnylink.completed:
|
||||
self._state = OnboardingState.SUNNYLINK_CONSENT
|
||||
elif not self._training_done:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
else:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
self._needs_initial_push = False
|
||||
|
||||
def _on_uninstall(self):
|
||||
ui_state.params.put_bool("DoUninstall", True)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
device.set_override_interactive_timeout(300)
|
||||
device.set_offroad_brightness(100)
|
||||
self._needs_initial_push = True
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
# FIXME: when nav stack sends hide event to widget 2 below on push, this needs to be moved
|
||||
device.set_override_interactive_timeout(None)
|
||||
device.set_offroad_brightness(None)
|
||||
|
||||
@property
|
||||
def completed(self) -> bool:
|
||||
return self._accepted_terms and self._sunnylink.completed and self._training_done
|
||||
|
||||
def _on_terms_declined(self):
|
||||
self._state = OnboardingState.DECLINE
|
||||
|
||||
def _on_decline_back(self):
|
||||
self._state = OnboardingState.TERMS
|
||||
return self._accepted_terms and self._sunnylink_consent_done and self._training_done
|
||||
|
||||
def close(self):
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", False)
|
||||
gui_app.pop_widget()
|
||||
ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", False)
|
||||
self._completed_callback()
|
||||
|
||||
def _on_terms_accepted(self):
|
||||
ui_state.params.put("HasAcceptedTerms", terms_version)
|
||||
ui_state.params.put("HasAcceptedTermsSP", terms_version_sp)
|
||||
if not self._sunnylink.completed:
|
||||
self._state = OnboardingState.SUNNYLINK_CONSENT
|
||||
self._accepted_terms = True
|
||||
if not self._sunnylink_consent_done:
|
||||
gui_app.push_widget(self._sunnylink_consent)
|
||||
elif not self._training_done:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
gui_app.push_widget(self._training_guide)
|
||||
else:
|
||||
self.close()
|
||||
|
||||
def _on_sunnylink_accepted(self):
|
||||
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
|
||||
ui_state.params.put_bool("SunnylinkEnabled", True)
|
||||
self._sunnylink_consent_done = True
|
||||
if not self._training_done:
|
||||
gui_app.push_widget(self._training_guide)
|
||||
else:
|
||||
self.close()
|
||||
|
||||
def _on_sunnylink_declined(self):
|
||||
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_declined)
|
||||
ui_state.params.put_bool("SunnylinkEnabled", False)
|
||||
self._sunnylink_consent_done = True
|
||||
if not self._training_done:
|
||||
gui_app.push_widget(self._training_guide)
|
||||
else:
|
||||
self.close()
|
||||
|
||||
def _on_completed_training(self):
|
||||
ui_state.params.put("CompletedTrainingVersion", training_version)
|
||||
self._training_done = True
|
||||
self.close()
|
||||
|
||||
def _render(self, _):
|
||||
rl.draw_rectangle_rec(self._rect, rl.BLACK)
|
||||
if self._state == OnboardingState.TERMS:
|
||||
self._terms.render(self._rect)
|
||||
elif self._state == OnboardingState.SUNNYLINK_CONSENT:
|
||||
self._sunnylink.render(self._rect)
|
||||
if self._sunnylink.completed:
|
||||
if not self._training_done:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
else:
|
||||
self.close()
|
||||
elif self._state == OnboardingState.ONBOARDING:
|
||||
if not self._training_done:
|
||||
self._training_guide.render(self._rect)
|
||||
else:
|
||||
self.close()
|
||||
elif self._state == OnboardingState.DECLINE:
|
||||
self._decline_page.render(self._rect)
|
||||
|
||||
# Deferred from show_event to avoid nested push_widget re-enable bug
|
||||
if self._needs_initial_push:
|
||||
self._needs_initial_push = False
|
||||
if self._accepted_terms and not self._sunnylink_consent_done:
|
||||
gui_app.push_widget(self._sunnylink_consent)
|
||||
elif self._accepted_terms and self._sunnylink_consent_done and not self._training_done:
|
||||
gui_app.push_widget(self._training_guide)
|
||||
|
||||
self._terms.render(self._rect)
|
||||
|
||||
@@ -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(username)
|
||||
else:
|
||||
dlg = BigDialog("", ssh_keys._error_message)
|
||||
gui_app.push_widget(dlg)
|
||||
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:
|
||||
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,19 +9,40 @@ 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.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.widgets.label import MiciLabel
|
||||
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
||||
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
|
||||
|
||||
|
||||
class ReviewTermsPage(TermsPage, NavScroller):
|
||||
"""TermsPage with NavWidget swipe-to-dismiss for reviewing in device settings."""
|
||||
def __init__(self):
|
||||
super().__init__(on_accept=self.dismiss, on_decline=self.dismiss)
|
||||
self._terms_header.set_visible(False)
|
||||
self._must_accept_card.set_visible(False)
|
||||
self._accept_button.set_visible(False)
|
||||
self._decline_button.set_visible(False)
|
||||
|
||||
|
||||
class ReviewTrainingGuide(TrainingGuide):
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
device.set_override_interactive_timeout(300)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
device.set_override_interactive_timeout(None)
|
||||
ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", False)
|
||||
|
||||
|
||||
class MiciFccModal(NavRawScrollPanel):
|
||||
def __init__(self, file_path: str | None = None, text: str | None = None):
|
||||
super().__init__()
|
||||
@@ -43,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"
|
||||
else:
|
||||
# TODO: check
|
||||
icon = "icons_mici/settings/comma_icon.png"
|
||||
|
||||
dlg: BigConfirmationDialogV2 | BigDialog = BigConfirmationDialogV2(f"slide to\n{action_text.lower()}", icon, red=red,
|
||||
exit_on_confirm=action_text == "reset",
|
||||
confirm_callback=confirm_callback)
|
||||
gui_app.push_widget(dlg)
|
||||
gui_app.push_widget(BigConfirmationDialog(f"slide to\n{action_text.lower()}", icon, confirm_callback, exit_on_confirm=exit_on_confirm, red=red))
|
||||
else:
|
||||
dlg = BigDialog(f"Disengage to {action_text}", "")
|
||||
gui_app.push_widget(dlg)
|
||||
gui_app.push_widget(BigDialog("", f"Disengage to {action_text}"))
|
||||
|
||||
|
||||
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):
|
||||
@@ -80,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)
|
||||
@@ -111,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
|
||||
@@ -137,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)
|
||||
@@ -169,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
|
||||
|
||||
@@ -290,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.set_click_callback(lambda: gui_app.push_widget(TrainingGuide(completed_callback=gui_app.pop_widget)))
|
||||
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.set_click_callback(lambda: gui_app.push_widget(TermsPage(on_accept=gui_app.pop_widget)))
|
||||
terms_btn.set_enabled(lambda: ui_state.is_offroad())
|
||||
terms_btn = BigButton("terms &\nconditions", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64))
|
||||
terms_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTermsPage()))
|
||||
|
||||
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
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigToggle
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiIcon
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType, ConnectStatus, SecurityType, normalize_ssid
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, ConnectStatus, SecurityType, normalize_ssid
|
||||
|
||||
|
||||
class WifiNetworkButton(BigButton):
|
||||
@@ -62,148 +58,3 @@ class WifiNetworkButton(BigButton):
|
||||
lock_x = icon_x + self._txt_icon.width - self._lock_txt.width + 7
|
||||
lock_y = icon_y + self._txt_icon.height - self._lock_txt.height + 8
|
||||
rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
|
||||
class NetworkLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._wifi_manager = WifiManager()
|
||||
self._wifi_manager.set_active(False)
|
||||
self._wifi_ui = WifiUIMici(self._wifi_manager)
|
||||
|
||||
self._wifi_manager.add_callbacks(
|
||||
networks_updated=self._on_network_updated,
|
||||
)
|
||||
|
||||
# ******** Tethering ********
|
||||
def tethering_toggle_callback(checked: bool):
|
||||
self._tethering_toggle_btn.set_enabled(False)
|
||||
self._tethering_password_btn.set_enabled(False)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
self._wifi_manager.set_tethering_active(checked)
|
||||
|
||||
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
|
||||
|
||||
def tethering_password_callback(password: str):
|
||||
if password:
|
||||
self._tethering_toggle_btn.set_enabled(False)
|
||||
self._tethering_password_btn.set_enabled(False)
|
||||
self._wifi_manager.set_tethering_password(password)
|
||||
|
||||
def tethering_password_clicked():
|
||||
tethering_password = self._wifi_manager.tethering_password
|
||||
dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8,
|
||||
confirm_callback=tethering_password_callback)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
txt_tethering = gui_app.texture("icons_mici/settings/network/tethering.png", 64, 54)
|
||||
self._tethering_password_btn = BigButton("tethering password", "", txt_tethering)
|
||||
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
|
||||
|
||||
# ******** Network Metered ********
|
||||
def network_metered_callback(value: str):
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
metered = {
|
||||
'default': MeteredType.UNKNOWN,
|
||||
'metered': MeteredType.YES,
|
||||
'unmetered': MeteredType.NO
|
||||
}.get(value, MeteredType.UNKNOWN)
|
||||
self._wifi_manager.set_current_network_metered(metered)
|
||||
|
||||
# TODO: signal for current network metered type when changing networks, this is wrong until you press it once
|
||||
# TODO: disable when not connected
|
||||
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
|
||||
self._wifi_button = WifiNetworkButton(self._wifi_manager)
|
||||
self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui))
|
||||
|
||||
# ******** Advanced settings ********
|
||||
# ******** Roaming toggle ********
|
||||
self._roaming_btn = BigParamControl("enable roaming", "GsmRoaming", toggle_callback=self._toggle_roaming)
|
||||
|
||||
# ******** APN settings ********
|
||||
self._apn_btn = BigButton("apn settings", "edit")
|
||||
self._apn_btn.set_click_callback(self._edit_apn)
|
||||
|
||||
# ******** Cellular metered toggle ********
|
||||
self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered", toggle_callback=self._toggle_cellular_metered)
|
||||
|
||||
# Main scroller ----------------------------------
|
||||
self._scroller.add_widgets([
|
||||
self._wifi_button,
|
||||
self._network_metered_btn,
|
||||
self._tethering_toggle_btn,
|
||||
self._tethering_password_btn,
|
||||
# /* Advanced settings
|
||||
self._roaming_btn,
|
||||
self._apn_btn,
|
||||
self._cellular_metered_btn,
|
||||
# */
|
||||
])
|
||||
|
||||
# Set initial config
|
||||
roaming_enabled = ui_state.params.get_bool("GsmRoaming")
|
||||
metered = ui_state.params.get_bool("GsmMetered")
|
||||
self._wifi_manager.update_gsm_settings(roaming_enabled, ui_state.params.get("GsmApn") or "", metered)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
# If not using prime SIM, show GSM settings and enable IPv4 forwarding
|
||||
show_cell_settings = ui_state.prime_state.get_type() in (PrimeType.NONE, PrimeType.LITE)
|
||||
self._wifi_manager.set_ipv4_forward(show_cell_settings)
|
||||
self._roaming_btn.set_visible(show_cell_settings)
|
||||
self._apn_btn.set_visible(show_cell_settings)
|
||||
self._cellular_metered_btn.set_visible(show_cell_settings)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._wifi_manager.set_active(True)
|
||||
|
||||
# Process wifi callbacks while at any point in the nav stack
|
||||
gui_app.add_nav_stack_tick(self._wifi_manager.process_callbacks)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._wifi_manager.set_active(False)
|
||||
|
||||
gui_app.remove_nav_stack_tick(self._wifi_manager.process_callbacks)
|
||||
|
||||
def _toggle_roaming(self, checked: bool):
|
||||
self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered"))
|
||||
|
||||
def _edit_apn(self):
|
||||
def update_apn(apn: str):
|
||||
apn = apn.strip()
|
||||
if apn == "":
|
||||
ui_state.params.remove("GsmApn")
|
||||
else:
|
||||
ui_state.params.put("GsmApn", apn)
|
||||
|
||||
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), apn, ui_state.params.get_bool("GsmMetered"))
|
||||
|
||||
current_apn = ui_state.params.get("GsmApn") or ""
|
||||
dlg = BigInputDialog("enter APN...", current_apn, minimum_length=0, confirm_callback=update_apn)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
def _toggle_cellular_metered(self, checked: bool):
|
||||
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), ui_state.params.get("GsmApn") or "", checked)
|
||||
|
||||
def _on_network_updated(self, networks: list[Network]):
|
||||
# Update tethering state
|
||||
tethering_active = self._wifi_manager.is_tethering_active()
|
||||
# TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons
|
||||
self._tethering_toggle_btn.set_enabled(True)
|
||||
self._tethering_password_btn.set_enabled(True)
|
||||
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
|
||||
self._tethering_toggle_btn.set_checked(tethering_active)
|
||||
|
||||
# Update network metered
|
||||
self._network_metered_btn.set_value(
|
||||
{
|
||||
MeteredType.UNKNOWN: 'default',
|
||||
MeteredType.YES: 'metered',
|
||||
MeteredType.NO: 'unmetered'
|
||||
}.get(self._wifi_manager.current_network_metered, 'default'))
|
||||
|
||||
154
selfdrive/ui/mici/layouts/settings/network/network_layout.py
Normal file
154
selfdrive/ui/mici/layouts/settings/network/network_layout.py
Normal file
@@ -0,0 +1,154 @@
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network import WifiNetworkButton
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigToggle
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType
|
||||
|
||||
|
||||
class NetworkLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._wifi_manager = WifiManager()
|
||||
self._wifi_manager.set_active(False)
|
||||
self._wifi_ui = WifiUIMici(self._wifi_manager)
|
||||
|
||||
self._wifi_manager.add_callbacks(
|
||||
networks_updated=self._on_network_updated,
|
||||
)
|
||||
|
||||
# ******** Tethering ********
|
||||
def tethering_toggle_callback(checked: bool):
|
||||
self._tethering_toggle_btn.set_enabled(False)
|
||||
self._tethering_password_btn.set_enabled(False)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
self._wifi_manager.set_tethering_active(checked)
|
||||
|
||||
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
|
||||
|
||||
def tethering_password_callback(password: str):
|
||||
if password:
|
||||
self._tethering_toggle_btn.set_enabled(False)
|
||||
self._tethering_password_btn.set_enabled(False)
|
||||
self._wifi_manager.set_tethering_password(password)
|
||||
|
||||
def tethering_password_clicked():
|
||||
tethering_password = self._wifi_manager.tethering_password
|
||||
dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8,
|
||||
confirm_callback=tethering_password_callback)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
txt_tethering = gui_app.texture("icons_mici/settings/network/tethering.png", 64, 54)
|
||||
self._tethering_password_btn = BigButton("tethering password", "", txt_tethering)
|
||||
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
|
||||
|
||||
# ******** Network Metered ********
|
||||
def network_metered_callback(value: str):
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
metered = {
|
||||
'default': MeteredType.UNKNOWN,
|
||||
'metered': MeteredType.YES,
|
||||
'unmetered': MeteredType.NO
|
||||
}.get(value, MeteredType.UNKNOWN)
|
||||
self._wifi_manager.set_current_network_metered(metered)
|
||||
|
||||
# TODO: signal for current network metered type when changing networks, this is wrong until you press it once
|
||||
# TODO: disable when not connected
|
||||
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
|
||||
self._wifi_button = WifiNetworkButton(self._wifi_manager)
|
||||
self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui))
|
||||
|
||||
# ******** Advanced settings ********
|
||||
# ******** Roaming toggle ********
|
||||
self._roaming_btn = BigParamControl("enable roaming", "GsmRoaming", toggle_callback=self._toggle_roaming)
|
||||
|
||||
# ******** APN settings ********
|
||||
self._apn_btn = BigButton("apn settings", "edit")
|
||||
self._apn_btn.set_click_callback(self._edit_apn)
|
||||
|
||||
# ******** Cellular metered toggle ********
|
||||
self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered", toggle_callback=self._toggle_cellular_metered)
|
||||
|
||||
# Main scroller ----------------------------------
|
||||
self._scroller.add_widgets([
|
||||
self._wifi_button,
|
||||
self._network_metered_btn,
|
||||
self._tethering_toggle_btn,
|
||||
self._tethering_password_btn,
|
||||
# /* Advanced settings
|
||||
self._roaming_btn,
|
||||
self._apn_btn,
|
||||
self._cellular_metered_btn,
|
||||
# */
|
||||
])
|
||||
|
||||
# Set initial config
|
||||
roaming_enabled = ui_state.params.get_bool("GsmRoaming")
|
||||
metered = ui_state.params.get_bool("GsmMetered")
|
||||
self._wifi_manager.update_gsm_settings(roaming_enabled, ui_state.params.get("GsmApn") or "", metered)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
# If not using prime SIM, show GSM settings and enable IPv4 forwarding
|
||||
show_cell_settings = ui_state.prime_state.get_type() in (PrimeType.NONE, PrimeType.LITE)
|
||||
self._wifi_manager.set_ipv4_forward(show_cell_settings)
|
||||
self._roaming_btn.set_visible(show_cell_settings)
|
||||
self._apn_btn.set_visible(show_cell_settings)
|
||||
self._cellular_metered_btn.set_visible(show_cell_settings)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._wifi_manager.set_active(True)
|
||||
|
||||
# Process wifi callbacks while at any point in the nav stack
|
||||
gui_app.add_nav_stack_tick(self._wifi_manager.process_callbacks)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._wifi_manager.set_active(False)
|
||||
|
||||
gui_app.remove_nav_stack_tick(self._wifi_manager.process_callbacks)
|
||||
|
||||
def _toggle_roaming(self, checked: bool):
|
||||
self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered"))
|
||||
|
||||
def _edit_apn(self):
|
||||
def update_apn(apn: str):
|
||||
apn = apn.strip()
|
||||
if apn == "":
|
||||
ui_state.params.remove("GsmApn")
|
||||
else:
|
||||
ui_state.params.put("GsmApn", apn)
|
||||
|
||||
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), apn, ui_state.params.get_bool("GsmMetered"))
|
||||
|
||||
current_apn = ui_state.params.get("GsmApn") or ""
|
||||
dlg = BigInputDialog("enter APN...", current_apn, minimum_length=0, confirm_callback=update_apn)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
def _toggle_cellular_metered(self, checked: bool):
|
||||
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), ui_state.params.get("GsmApn") or "", checked)
|
||||
|
||||
def _on_network_updated(self, networks: list[Network]):
|
||||
# Update tethering state
|
||||
tethering_active = self._wifi_manager.is_tethering_active()
|
||||
# TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons
|
||||
self._tethering_toggle_btn.set_enabled(True)
|
||||
self._tethering_password_btn.set_enabled(True)
|
||||
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
|
||||
self._tethering_toggle_btn.set_checked(tethering_active)
|
||||
|
||||
# Update network metered
|
||||
self._network_metered_btn.set_value(
|
||||
{
|
||||
MeteredType.UNKNOWN: 'default',
|
||||
MeteredType.YES: 'metered',
|
||||
MeteredType.NO: 'unmetered'
|
||||
}.get(self._wifi_manager.current_network_metered, 'default'))
|
||||
@@ -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,10 +317,22 @@ class WifiUIMici(NavScroller):
|
||||
btn.set_click_callback(lambda ssid=network.ssid: self._connect_to_network(ssid))
|
||||
self._scroller.add_widget(btn)
|
||||
|
||||
# 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)
|
||||
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)
|
||||
@@ -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))
|
||||
|
||||
@@ -2,7 +2,7 @@ from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network import NetworkLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.network_layout import NetworkLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout
|
||||
@@ -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([
|
||||
|
||||
@@ -231,7 +231,7 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
|
||||
self._alpha_filter.update(0 if alert is None else 1)
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
ui_state.onroad_brightness_handle_alerts(ui_state.started, alert)
|
||||
ui_state.onroad_brightness_handle_alerts(ui_state, alert)
|
||||
|
||||
if alert is None:
|
||||
# If still animating out, keep the previous alert
|
||||
@@ -272,8 +272,8 @@ 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.Color(255, 255, 255, int(icon_alpha * self._alpha_filter.x)))
|
||||
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:
|
||||
# draw top gradient for alert text at top
|
||||
|
||||
@@ -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):
|
||||
@@ -251,7 +251,7 @@ class AugmentedRoadView(CameraView):
|
||||
# Draw darkened background and text if not onroad
|
||||
if not ui_state.started:
|
||||
rl.draw_rectangle(int(self.rect.x), int(self.rect.y), int(self.rect.width), int(self.rect.height), rl.Color(0, 0, 0, 175))
|
||||
self._offroad_label.render(self._content_rect)
|
||||
self._offroad_label.render(self._rect)
|
||||
|
||||
# publish uiDebug
|
||||
msg = messaging.new_message('uiDebug')
|
||||
|
||||
@@ -155,11 +155,11 @@ class CameraView(Widget):
|
||||
# Prevent old frames from showing when going onroad. Qt has a separate thread
|
||||
# which drains the VisionIpcClient SubSocket for us. Re-connecting is not enough
|
||||
# and only clears internal buffers, not the message queue.
|
||||
self.frame = None
|
||||
self.available_streams.clear()
|
||||
if self.client:
|
||||
del self.client
|
||||
self.client = VisionIpcClient(self._name, self._stream_type, conflate=True)
|
||||
self.frame = None
|
||||
|
||||
def _set_placeholder_color(self, color: rl.Color):
|
||||
"""Set a placeholder color to be drawn when no frame is available."""
|
||||
|
||||
@@ -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,15 +88,14 @@ 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.Color(255, 255, 255, int(255 * self._fade_filter.x)))
|
||||
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.Color(255, 255, 255, int(255 * 0.9 * self._fade_filter.x)))
|
||||
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:
|
||||
source_rect = rl.Rectangle(0, 0, self._dm_cone.width, self._dm_cone.height)
|
||||
|
||||
@@ -172,8 +172,7 @@ class HudRenderer(Widget):
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
"""Render HUD elements to the screen."""
|
||||
|
||||
if ui_state.sm['controlsState'].lateralControlState.which() != 'angleState':
|
||||
self._torque_bar.render(rect)
|
||||
self._torque_bar.render(rect)
|
||||
|
||||
if self.is_cruise_set:
|
||||
self._draw_set_speed(rect)
|
||||
@@ -222,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."""
|
||||
|
||||
@@ -145,6 +145,9 @@ def arc_bar_pts(cx: float, cy: float,
|
||||
return pts
|
||||
|
||||
|
||||
DEFAULT_MAX_LAT_ACCEL = 3.0 # m/s^2
|
||||
|
||||
|
||||
class TorqueBar(Widget):
|
||||
def __init__(self, demo: bool = False, scale: float = 1.0, always: bool = False):
|
||||
super().__init__()
|
||||
@@ -167,16 +170,23 @@ class TorqueBar(Widget):
|
||||
controls_state = ui_state.sm['controlsState']
|
||||
car_state = ui_state.sm['carState']
|
||||
live_parameters = ui_state.sm['liveParameters']
|
||||
lateral_acceleration = controls_state.curvature * car_state.vEgo ** 2 - live_parameters.roll * ACCELERATION_DUE_TO_GRAVITY
|
||||
# TODO: pull from carparams
|
||||
max_lateral_acceleration = 3
|
||||
car_control = ui_state.sm['carControl']
|
||||
|
||||
# from selfdrived
|
||||
# Include lateral accel error in estimated torque utilization
|
||||
actual_lateral_accel = controls_state.curvature * car_state.vEgo ** 2
|
||||
desired_lateral_accel = controls_state.desiredCurvature * car_state.vEgo ** 2
|
||||
accel_diff = (desired_lateral_accel - actual_lateral_accel)
|
||||
|
||||
self._torque_filter.update(min(max(lateral_acceleration / max_lateral_acceleration + accel_diff, -1), 1))
|
||||
# Include road roll in estimated torque utilization
|
||||
# Roll is less accurate near standstill, so reduce its effect at low speed
|
||||
roll_compensation = live_parameters.roll * ACCELERATION_DUE_TO_GRAVITY * np.interp(car_state.vEgo, [5, 15], [0.0, 1.0])
|
||||
lateral_acceleration = actual_lateral_accel - roll_compensation
|
||||
max_lateral_acceleration = ui_state.CP.maxLateralAccel if ui_state.CP else DEFAULT_MAX_LAT_ACCEL
|
||||
|
||||
if not car_control.latActive:
|
||||
self._torque_filter.update(0.0)
|
||||
else:
|
||||
self._torque_filter.update(np.clip((lateral_acceleration + accel_diff) / max_lateral_acceleration, -1, 1))
|
||||
else:
|
||||
self._torque_filter.update(-ui_state.sm['carOutput'].actuatorsOutput.torque)
|
||||
|
||||
|
||||
@@ -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",
|
||||
@@ -68,9 +68,11 @@ def test_dialogs_do_not_leak():
|
||||
|
||||
for ctor in (
|
||||
# mici
|
||||
MiciDriverCameraDialog, MiciTrainingGuide, MiciOnboardingWindow, MiciPairingDialog,
|
||||
MiciDriverCameraDialog, MiciPairingDialog,
|
||||
lambda: MiciTrainingGuide(lambda: None),
|
||||
lambda: MiciOnboardingWindow(lambda: None),
|
||||
lambda: BigDialog("test", "test"),
|
||||
lambda: BigConfirmationDialogV2("test", "icons_mici/settings/network/new/trash.png"),
|
||||
lambda: 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,19 +107,18 @@ 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
|
||||
self._shake_start: float | None = None
|
||||
self._grow_animation_until: float | None = None
|
||||
|
||||
self._rotate_icon_t: float | None = None
|
||||
|
||||
@@ -132,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:
|
||||
@@ -145,9 +144,12 @@ class BigButton(Widget):
|
||||
self._txt_pressed_bg = gui_app.texture("icons_mici/buttons/button_rectangle_pressed.png", 402, 180)
|
||||
self._txt_disabled_bg = gui_app.texture("icons_mici/buttons/button_rectangle_disabled.png", 402, 180)
|
||||
|
||||
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
|
||||
super().set_touch_valid_callback(lambda: touch_callback() and self._grow_animation_until is None)
|
||||
|
||||
def _width_hint(self) -> int:
|
||||
# Single line if scrolling, so hide behind icon if exists
|
||||
icon_size = self._icon_size[0] if self._txt_icon and self._scroll and self.value else 0
|
||||
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):
|
||||
@@ -182,12 +184,17 @@ class BigButton(Widget):
|
||||
def trigger_shake(self):
|
||||
self._shake_start = rl.get_time()
|
||||
|
||||
def trigger_grow_animation(self, duration: float = 0.65):
|
||||
self._grow_animation_until = rl.get_time() + duration
|
||||
|
||||
@property
|
||||
def _shake_offset(self) -> float:
|
||||
SHAKE_DURATION = 0.5
|
||||
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
|
||||
@@ -197,6 +204,10 @@ class BigButton(Widget):
|
||||
super().set_position(x + self._shake_offset, y)
|
||||
|
||||
def _handle_background(self) -> tuple[rl.Texture, float, float, float]:
|
||||
if self._grow_animation_until is not None:
|
||||
if rl.get_time() >= self._grow_animation_until:
|
||||
self._grow_animation_until = None
|
||||
|
||||
# draw _txt_default_bg
|
||||
txt_bg = self._txt_default_bg
|
||||
if not self.enabled:
|
||||
@@ -204,7 +215,7 @@ class BigButton(Widget):
|
||||
elif self.is_pressed:
|
||||
txt_bg = self._txt_pressed_bg
|
||||
|
||||
scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed else 1.0)
|
||||
scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed or self._grow_animation_until is not None else 1.0)
|
||||
btn_x = self._rect.x + (self._rect.width * (1 - scale)) / 2
|
||||
btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2
|
||||
return txt_bg, btn_x, btn_y, scale
|
||||
@@ -324,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):
|
||||
@@ -359,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):
|
||||
@@ -103,11 +75,12 @@ class BigInputDialog(BigDialogBase):
|
||||
hint: str,
|
||||
default_text: str = "",
|
||||
minimum_length: int = 1,
|
||||
confirm_callback: Callable[[str], None] | None = None):
|
||||
confirm_callback: Callable[[str], None] | None = None,
|
||||
auto_return_to_letters: str = ""):
|
||||
super().__init__()
|
||||
self._hint_label = UnifiedLabel(hint, font_size=35, text_color=rl.Color(255, 255, 255, int(255 * 0.35)),
|
||||
font_weight=FontWeight.MEDIUM)
|
||||
self._keyboard = MiciKeyboard()
|
||||
self._keyboard = MiciKeyboard(auto_return_to_letters=auto_return_to_letters)
|
||||
self._keyboard.set_text(default_text)
|
||||
self._keyboard.set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack + NavWidget
|
||||
self._minimum_length = minimum_length
|
||||
@@ -157,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
|
||||
@@ -180,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
|
||||
@@ -189,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
|
||||
@@ -214,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)
|
||||
@@ -253,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):
|
||||
|
||||
@@ -118,7 +118,7 @@ class AlertRenderer(Widget):
|
||||
alert = self.get_alert(ui_state.sm)
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
ui_state.onroad_brightness_handle_alerts(ui_state.started, alert)
|
||||
ui_state.onroad_brightness_handle_alerts(ui_state, alert)
|
||||
|
||||
if not alert:
|
||||
return
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import option_item_sp, ToggleActionSP
|
||||
|
||||
ONROAD_BRIGHTNESS_TIMER_VALUES = {0: 15, 1: 30, **{i: (i - 1) * 60 for i in range(2, 12)}}
|
||||
from openpilot.sunnypilot.system.params_migration import ONROAD_BRIGHTNESS_TIMER_VALUES
|
||||
|
||||
|
||||
class OnroadBrightness(IntEnum):
|
||||
@@ -46,7 +45,7 @@ class DisplayLayout(Widget):
|
||||
title=lambda: tr("Onroad Brightness Delay"),
|
||||
description="",
|
||||
min_value=0,
|
||||
max_value=11,
|
||||
max_value=15,
|
||||
value_change_step=1,
|
||||
value_map=ONROAD_BRIGHTNESS_TIMER_VALUES,
|
||||
label_callback=lambda value: f"{value} s" if value < 60 else f"{int(value/60)} m",
|
||||
@@ -92,7 +91,11 @@ class DisplayLayout(Widget):
|
||||
if isinstance(_item.action_item, ToggleActionSP) and _item.action_item.toggle.param_key is not None:
|
||||
_item.action_item.set_state(self._params.get_bool(_item.action_item.toggle.param_key))
|
||||
elif isinstance(_item.action_item, OptionControlSP) and _item.action_item.param_key is not None:
|
||||
_item.action_item.set_value(self._params.get(_item.action_item.param_key, return_default=True))
|
||||
raw_value = self._params.get(_item.action_item.param_key, return_default=True)
|
||||
if _item.action_item.value_map:
|
||||
reverse_map = {v: k for k, v in _item.action_item.value_map.items()}
|
||||
raw_value = reverse_map.get(raw_value, _item.action_item.current_value)
|
||||
_item.action_item.set_value(raw_value)
|
||||
|
||||
brightness_val = self._params.get("OnroadScreenOffBrightness", return_default=True)
|
||||
self._onroad_brightness_timer.action_item.set_enabled(brightness_val not in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK))
|
||||
|
||||
@@ -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)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user