Merge branch 'master' into hyundai-radar-tracks

This commit is contained in:
royjr
2026-03-27 16:11:57 -07:00
244 changed files with 6834 additions and 10562 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
View File

@@ -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
View File

@@ -0,0 +1 @@
3.12.13

8
Jenkinsfile vendored
View File

@@ -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]),
])
},

View File

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

View File

@@ -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
View File

@@ -1 +0,0 @@
*.cpp

View File

@@ -1,4 +1,4 @@
Import('env', 'envCython', 'arch')
Import('env', 'envCython')
common_libs = [
'params.cc',

View File

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

View File

@@ -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"}},

View File

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

View File

@@ -1,2 +0,0 @@
transformations
transformations.cpp

View File

@@ -1 +1 @@
#define COMMA_VERSION "0.10.4"
#define COMMA_VERSION "0.11.1"

View File

@@ -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",
]

View File

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

Submodule panda updated: f5f296c65c...6ddc631bdd

View File

@@ -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"

View File

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

View File

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

View File

@@ -1,4 +1,2 @@
*.cc
fonts/*.fnt
fonts/*.png
translations_assets.qrc

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1 +0,0 @@
*.bz2

View File

@@ -1,2 +0,0 @@
calibration_param
traces

View File

@@ -1,2 +0,0 @@
params_learner
paramsd

View File

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

View File

@@ -1 +0,0 @@
out/

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ docker_out/
process_replay/diff.txt
process_replay/model_diff.txt
process_replay/fakedata/
valgrind_logs.txt
*.bz2
*.hevc

View File

@@ -1 +0,0 @@
fakedata/

View File

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

View File

@@ -1 +1,4 @@
installer/installers/*
tests/diff/report
.coverage

View File

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

View File

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

View File

@@ -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:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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:

View File

@@ -56,7 +56,7 @@ class MiciMainLayout(Scroller):
gui_app.push_widget(self)
# Start onboarding if terms or training not completed, make sure to push after self
self._onboarding_window = OnboardingWindow()
self._onboarding_window = OnboardingWindow(lambda: gui_app.pop_widgets_to(self))
if not self._onboarding_window.completed:
gui_app.push_widget(self._onboarding_window)
@@ -82,7 +82,7 @@ class MiciMainLayout(Scroller):
def _handle_transitions(self):
# Don't pop if onboarding
if gui_app.get_active_widget() == self._onboarding_window:
if gui_app.widget_in_stack(self._onboarding_window):
return
if ui_state.started != self._prev_onroad:
@@ -108,7 +108,7 @@ class MiciMainLayout(Scroller):
def _on_interactive_timeout(self):
# Don't pop if onboarding
if gui_app.get_active_widget() == self._onboarding_window:
if gui_app.widget_in_stack(self._onboarding_window):
return
if ui_state.started:

View File

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

View File

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

View File

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

View File

@@ -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(),

View File

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

View File

@@ -1,13 +1,9 @@
import pyray as rl
from openpilot.system.ui.widgets.scroller import NavScroller
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigToggle
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiIcon
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType, ConnectStatus, SecurityType, normalize_ssid
from openpilot.system.ui.lib.wifi_manager import WifiManager, ConnectStatus, SecurityType, normalize_ssid
class WifiNetworkButton(BigButton):
@@ -62,148 +58,3 @@ class WifiNetworkButton(BigButton):
lock_x = icon_x + self._txt_icon.width - self._lock_txt.width + 7
lock_y = icon_y + self._txt_icon.height - self._lock_txt.height + 8
rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, 1.0, rl.WHITE)
class NetworkLayoutMici(NavScroller):
def __init__(self):
super().__init__()
self._wifi_manager = WifiManager()
self._wifi_manager.set_active(False)
self._wifi_ui = WifiUIMici(self._wifi_manager)
self._wifi_manager.add_callbacks(
networks_updated=self._on_network_updated,
)
# ******** Tethering ********
def tethering_toggle_callback(checked: bool):
self._tethering_toggle_btn.set_enabled(False)
self._tethering_password_btn.set_enabled(False)
self._network_metered_btn.set_enabled(False)
self._wifi_manager.set_tethering_active(checked)
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
def tethering_password_callback(password: str):
if password:
self._tethering_toggle_btn.set_enabled(False)
self._tethering_password_btn.set_enabled(False)
self._wifi_manager.set_tethering_password(password)
def tethering_password_clicked():
tethering_password = self._wifi_manager.tethering_password
dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8,
confirm_callback=tethering_password_callback)
gui_app.push_widget(dlg)
txt_tethering = gui_app.texture("icons_mici/settings/network/tethering.png", 64, 54)
self._tethering_password_btn = BigButton("tethering password", "", txt_tethering)
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
# ******** Network Metered ********
def network_metered_callback(value: str):
self._network_metered_btn.set_enabled(False)
metered = {
'default': MeteredType.UNKNOWN,
'metered': MeteredType.YES,
'unmetered': MeteredType.NO
}.get(value, MeteredType.UNKNOWN)
self._wifi_manager.set_current_network_metered(metered)
# TODO: signal for current network metered type when changing networks, this is wrong until you press it once
# TODO: disable when not connected
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
self._network_metered_btn.set_enabled(False)
self._wifi_button = WifiNetworkButton(self._wifi_manager)
self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui))
# ******** Advanced settings ********
# ******** Roaming toggle ********
self._roaming_btn = BigParamControl("enable roaming", "GsmRoaming", toggle_callback=self._toggle_roaming)
# ******** APN settings ********
self._apn_btn = BigButton("apn settings", "edit")
self._apn_btn.set_click_callback(self._edit_apn)
# ******** Cellular metered toggle ********
self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered", toggle_callback=self._toggle_cellular_metered)
# Main scroller ----------------------------------
self._scroller.add_widgets([
self._wifi_button,
self._network_metered_btn,
self._tethering_toggle_btn,
self._tethering_password_btn,
# /* Advanced settings
self._roaming_btn,
self._apn_btn,
self._cellular_metered_btn,
# */
])
# Set initial config
roaming_enabled = ui_state.params.get_bool("GsmRoaming")
metered = ui_state.params.get_bool("GsmMetered")
self._wifi_manager.update_gsm_settings(roaming_enabled, ui_state.params.get("GsmApn") or "", metered)
def _update_state(self):
super()._update_state()
# If not using prime SIM, show GSM settings and enable IPv4 forwarding
show_cell_settings = ui_state.prime_state.get_type() in (PrimeType.NONE, PrimeType.LITE)
self._wifi_manager.set_ipv4_forward(show_cell_settings)
self._roaming_btn.set_visible(show_cell_settings)
self._apn_btn.set_visible(show_cell_settings)
self._cellular_metered_btn.set_visible(show_cell_settings)
def show_event(self):
super().show_event()
self._wifi_manager.set_active(True)
# Process wifi callbacks while at any point in the nav stack
gui_app.add_nav_stack_tick(self._wifi_manager.process_callbacks)
def hide_event(self):
super().hide_event()
self._wifi_manager.set_active(False)
gui_app.remove_nav_stack_tick(self._wifi_manager.process_callbacks)
def _toggle_roaming(self, checked: bool):
self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered"))
def _edit_apn(self):
def update_apn(apn: str):
apn = apn.strip()
if apn == "":
ui_state.params.remove("GsmApn")
else:
ui_state.params.put("GsmApn", apn)
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), apn, ui_state.params.get_bool("GsmMetered"))
current_apn = ui_state.params.get("GsmApn") or ""
dlg = BigInputDialog("enter APN...", current_apn, minimum_length=0, confirm_callback=update_apn)
gui_app.push_widget(dlg)
def _toggle_cellular_metered(self, checked: bool):
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), ui_state.params.get("GsmApn") or "", checked)
def _on_network_updated(self, networks: list[Network]):
# Update tethering state
tethering_active = self._wifi_manager.is_tethering_active()
# TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons
self._tethering_toggle_btn.set_enabled(True)
self._tethering_password_btn.set_enabled(True)
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
self._tethering_toggle_btn.set_checked(tethering_active)
# Update network metered
self._network_metered_btn.set_value(
{
MeteredType.UNKNOWN: 'default',
MeteredType.YES: 'metered',
MeteredType.NO: 'unmetered'
}.get(self._wifi_manager.current_network_metered, 'default'))

View File

@@ -0,0 +1,154 @@
from openpilot.system.ui.widgets.scroller import NavScroller
from openpilot.selfdrive.ui.mici.layouts.settings.network import WifiNetworkButton
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigToggle
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType
class NetworkLayoutMici(NavScroller):
def __init__(self):
super().__init__()
self._wifi_manager = WifiManager()
self._wifi_manager.set_active(False)
self._wifi_ui = WifiUIMici(self._wifi_manager)
self._wifi_manager.add_callbacks(
networks_updated=self._on_network_updated,
)
# ******** Tethering ********
def tethering_toggle_callback(checked: bool):
self._tethering_toggle_btn.set_enabled(False)
self._tethering_password_btn.set_enabled(False)
self._network_metered_btn.set_enabled(False)
self._wifi_manager.set_tethering_active(checked)
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
def tethering_password_callback(password: str):
if password:
self._tethering_toggle_btn.set_enabled(False)
self._tethering_password_btn.set_enabled(False)
self._wifi_manager.set_tethering_password(password)
def tethering_password_clicked():
tethering_password = self._wifi_manager.tethering_password
dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8,
confirm_callback=tethering_password_callback)
gui_app.push_widget(dlg)
txt_tethering = gui_app.texture("icons_mici/settings/network/tethering.png", 64, 54)
self._tethering_password_btn = BigButton("tethering password", "", txt_tethering)
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
# ******** Network Metered ********
def network_metered_callback(value: str):
self._network_metered_btn.set_enabled(False)
metered = {
'default': MeteredType.UNKNOWN,
'metered': MeteredType.YES,
'unmetered': MeteredType.NO
}.get(value, MeteredType.UNKNOWN)
self._wifi_manager.set_current_network_metered(metered)
# TODO: signal for current network metered type when changing networks, this is wrong until you press it once
# TODO: disable when not connected
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
self._network_metered_btn.set_enabled(False)
self._wifi_button = WifiNetworkButton(self._wifi_manager)
self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui))
# ******** Advanced settings ********
# ******** Roaming toggle ********
self._roaming_btn = BigParamControl("enable roaming", "GsmRoaming", toggle_callback=self._toggle_roaming)
# ******** APN settings ********
self._apn_btn = BigButton("apn settings", "edit")
self._apn_btn.set_click_callback(self._edit_apn)
# ******** Cellular metered toggle ********
self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered", toggle_callback=self._toggle_cellular_metered)
# Main scroller ----------------------------------
self._scroller.add_widgets([
self._wifi_button,
self._network_metered_btn,
self._tethering_toggle_btn,
self._tethering_password_btn,
# /* Advanced settings
self._roaming_btn,
self._apn_btn,
self._cellular_metered_btn,
# */
])
# Set initial config
roaming_enabled = ui_state.params.get_bool("GsmRoaming")
metered = ui_state.params.get_bool("GsmMetered")
self._wifi_manager.update_gsm_settings(roaming_enabled, ui_state.params.get("GsmApn") or "", metered)
def _update_state(self):
super()._update_state()
# If not using prime SIM, show GSM settings and enable IPv4 forwarding
show_cell_settings = ui_state.prime_state.get_type() in (PrimeType.NONE, PrimeType.LITE)
self._wifi_manager.set_ipv4_forward(show_cell_settings)
self._roaming_btn.set_visible(show_cell_settings)
self._apn_btn.set_visible(show_cell_settings)
self._cellular_metered_btn.set_visible(show_cell_settings)
def show_event(self):
super().show_event()
self._wifi_manager.set_active(True)
# Process wifi callbacks while at any point in the nav stack
gui_app.add_nav_stack_tick(self._wifi_manager.process_callbacks)
def hide_event(self):
super().hide_event()
self._wifi_manager.set_active(False)
gui_app.remove_nav_stack_tick(self._wifi_manager.process_callbacks)
def _toggle_roaming(self, checked: bool):
self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered"))
def _edit_apn(self):
def update_apn(apn: str):
apn = apn.strip()
if apn == "":
ui_state.params.remove("GsmApn")
else:
ui_state.params.put("GsmApn", apn)
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), apn, ui_state.params.get_bool("GsmMetered"))
current_apn = ui_state.params.get("GsmApn") or ""
dlg = BigInputDialog("enter APN...", current_apn, minimum_length=0, confirm_callback=update_apn)
gui_app.push_widget(dlg)
def _toggle_cellular_metered(self, checked: bool):
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), ui_state.params.get("GsmApn") or "", checked)
def _on_network_updated(self, networks: list[Network]):
# Update tethering state
tethering_active = self._wifi_manager.is_tethering_active()
# TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons
self._tethering_toggle_btn.set_enabled(True)
self._tethering_password_btn.set_enabled(True)
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
self._tethering_toggle_btn.set_checked(tethering_active)
# Update network metered
self._network_metered_btn.set_value(
{
MeteredType.UNKNOWN: 'default',
MeteredType.YES: 'metered',
MeteredType.NO: 'unmetered'
}.get(self._wifi_manager.current_network_metered, 'default'))

View File

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

View File

@@ -2,7 +2,7 @@ from openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller import NavScroller
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.network import NetworkLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.network.network_layout import NetworkLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton
from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout
@@ -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([

View File

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

View File

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

View File

@@ -155,11 +155,11 @@ class CameraView(Widget):
# Prevent old frames from showing when going onroad. Qt has a separate thread
# which drains the VisionIpcClient SubSocket for us. Re-connecting is not enough
# and only clears internal buffers, not the message queue.
self.frame = None
self.available_streams.clear()
if self.client:
del self.client
self.client = VisionIpcClient(self._name, self._stream_type, conflate=True)
self.frame = None
def _set_placeholder_color(self, color: rl.Color):
"""Set a placeholder color to be drawn when no frame is available."""

View File

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

View File

@@ -172,8 +172,7 @@ class HudRenderer(Widget):
def _render(self, rect: rl.Rectangle) -> None:
"""Render HUD elements to the screen."""
if ui_state.sm['controlsState'].lateralControlState.which() != 'angleState':
self._torque_bar.render(rect)
self._torque_bar.render(rect)
if self.is_cruise_set:
self._draw_set_speed(rect)
@@ -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."""

View File

@@ -145,6 +145,9 @@ def arc_bar_pts(cx: float, cy: float,
return pts
DEFAULT_MAX_LAT_ACCEL = 3.0 # m/s^2
class TorqueBar(Widget):
def __init__(self, demo: bool = False, scale: float = 1.0, always: bool = False):
super().__init__()
@@ -167,16 +170,23 @@ class TorqueBar(Widget):
controls_state = ui_state.sm['controlsState']
car_state = ui_state.sm['carState']
live_parameters = ui_state.sm['liveParameters']
lateral_acceleration = controls_state.curvature * car_state.vEgo ** 2 - live_parameters.roll * ACCELERATION_DUE_TO_GRAVITY
# TODO: pull from carparams
max_lateral_acceleration = 3
car_control = ui_state.sm['carControl']
# from selfdrived
# Include lateral accel error in estimated torque utilization
actual_lateral_accel = controls_state.curvature * car_state.vEgo ** 2
desired_lateral_accel = controls_state.desiredCurvature * car_state.vEgo ** 2
accel_diff = (desired_lateral_accel - actual_lateral_accel)
self._torque_filter.update(min(max(lateral_acceleration / max_lateral_acceleration + accel_diff, -1), 1))
# Include road roll in estimated torque utilization
# Roll is less accurate near standstill, so reduce its effect at low speed
roll_compensation = live_parameters.roll * ACCELERATION_DUE_TO_GRAVITY * np.interp(car_state.vEgo, [5, 15], [0.0, 1.0])
lateral_acceleration = actual_lateral_accel - roll_compensation
max_lateral_acceleration = ui_state.CP.maxLateralAccel if ui_state.CP else DEFAULT_MAX_LAT_ACCEL
if not car_control.latActive:
self._torque_filter.update(0.0)
else:
self._torque_filter.update(np.clip((lateral_acceleration + accel_diff) / max_lateral_acceleration, -1, 1))
else:
self._torque_filter.update(-ui_state.sm['carOutput'].actuatorsOutput.torque)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,

View File

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