Compare commits

...

39 Commits

Author SHA1 Message Date
Jason Wen ed8397c6d4 re-port over 2025-08-04 15:48:00 -04:00
Maxime Desroches 8cce8cf3f3 ui: keyboard improvements (#35906)
* better

* miss this one
2025-08-02 19:01:59 -07:00
Simon Kuang 0b855a93d7 scons: support build on single processor (#35904)
Update SConstruct
2025-08-02 16:50:45 -07:00
Jason Young 8c78749846 sim: fix "msg not found" errors (#35903)
* garbage-collect CRUISE_PARAMS

* follow GEARBOX message refactor
2025-08-02 19:10:49 -04:00
Adeeb Shihadeh aa2a3b3c8f hw: remove unused volume properties 2025-08-02 16:08:58 -07:00
Adeeb Shihadeh ba2dced54c Revert "LogReader: wrap events to cache which() (#35882)"
This reverts commit 0ebee55050.
2025-08-02 15:53:20 -07:00
Shane Smiskol 2e15ac5f4f test manager in CI (#35900)
* test manager

* not now

* try

* fix
2025-08-02 13:18:30 -07:00
Shane Smiskol c92add1280 process replay: don't wait for process to start (#35897)
* hmm

* test proc replay determinism

* clean up

* rm

* clean up
2025-08-02 12:34:13 -07:00
Adeeb Shihadeh bab251b287 fix conversions import path (#35899) 2025-08-02 12:02:17 -07:00
DevTekVE 9dc98b36be refactor: cleanup gravity constant handling (#35866)
* refactor: move lateral methods from init to lateral.py (#2594)

* Extracting lateral methods to lateral.py

* cleaning

* more cleaning

* more cleaning

* Making sure it remains where it should

* Leave rate_limit where it belongs

* Moving things to `car/controls/`

* Moving rate limit to get a taste of the changes

* clean

* copy verbatim

* clean up

* more

* now we can format

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>

* No need to change order of import

* refactor: consolidate ACCELERATION_DUE_TO_GRAVITY import path

* bump opendbc

* update refs

* don't import from opendbc

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>
Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2025-08-02 11:20:18 -07:00
Shane Smiskol 313f36712c process replay: lock polled socket only (#35887)
* stash

* Revert "stash"

This reverts commit 333818b80f498e8e3dac3c1cd36e669e97521d52.

* works for paramsd

* INSANE

* format

* fater

* clean up

* more

* huh i thought order matterred?

* clean that up

* can remove this

* cmt

* check isisntance

* rename

* clean up

* clean up

* more

* more!

* sounds better
2025-08-02 00:45:29 -07:00
Maxime Desroches 3ff874d6c2 ui: fix keyboard lint 2025-08-02 00:24:52 -07:00
Maxime Desroches eb751a3804 setup: convert to raylib touch api (#35862)
* first

* lint

* c

* simple first

* btn

* n

* more

* more

* bring back
2025-08-02 00:22:28 -07:00
Shane Smiskol 5a8e3470ff selfdrived: feed PoseCalibrator with updates (#35893)
this is also slow
2025-08-02 00:09:54 -07:00
Shane Smiskol 07909906d4 controlsd: speed up number checking (#35890)
Update controlsd.py
2025-08-02 00:08:18 -07:00
Shane Smiskol 7c87ada8d8 Simplify radarFault handling (#35891)
* Revert "Fix up `radarFault` handling (#35880)"

This reverts commit 4d01b7bec8.

* Reapply "Fix up `radarFault` handling (#35880)"

This reverts commit 597d7ec1ed78206035b924a6e8464cd9239b5db4.

* can do this

* yeah this is fine
2025-08-01 23:55:16 -07:00
Shane Smiskol bdd6ff4f3e process replay: remove frequency based recv callback (#35886)
* wtf is going on?

* rm it

* default
2025-08-01 21:46:32 -07:00
Shane Smiskol f2e100b0e1 process replay: clean up recv callbacks (#35889)
clean up callbacks
2025-08-01 21:36:15 -07:00
Shane Smiskol 8b0bfd7910 match on /test/ 2025-08-01 20:52:36 -07:00
Shane Smiskol db55f1275d process replay: set selfdrived main_pub (#35885)
* save 1-2s for full route

* now more than halve the time on top of previous speedup!

* stash

* default should be most common!

* revert

* revert

* clean up

* clean up

* clean up

* clean up
2025-08-01 20:49:45 -07:00
Shane Smiskol 8f9ee43d34 process replay: flip main_pub_drained default 2025-08-01 20:44:33 -07:00
Shane Smiskol 37c4ee1532 process replay: only enter prefix when interacting with process (#35884)
* save 1-2s for full route

* cu

* stock

* Revert "stock"

This reverts commit 7cfb550817b124c3085cf005fda8c102ae53ae9d.

* clean up
2025-08-01 20:13:02 -07:00
Shane Smiskol 0ebee55050 LogReader: wrap events to cache which() (#35882)
* speed up lr

* lazy caching

* clean up

* it fast

* stash

* stash

* chatgpt code is bad as usual

* clean up

* clean up

* clean up

* clean up

* clean up

* clean up

* match behavior

* cmt
2025-08-01 19:07:16 -07:00
Maxime Desroches cb5299be5a ui: adapt network to raylib touch api (#35881)
* start

* for now

* con

* more
2025-08-01 18:40:43 -07:00
Shane Smiskol 5c73681be8 process replay: rm dummy sockets (#35883)
* rm dummy sockets

* debug

* clean up

* cu
2025-08-01 18:38:42 -07:00
Shane Smiskol dd09c4f341 process replay: speed up startup (#35879)
* format

* containers might not be set

* opts

* halves startup time for 12 procs (1.6 to 0.8s)

* stash

* clean up

* who knew going through entire list of msgs each time is so slow

* rewrite this to be more readable

* speed up lr

* clean up

* more

* more
2025-08-01 17:51:39 -07:00
Adeeb Shihadeh 4d01b7bec8 Fix up radarFault handling (#35880)
* fixup radarFault handling

* catch all

---------

Co-authored-by: Comma Device <device@comma.ai>
2025-08-01 16:27:26 -07:00
Maxime Desroches 42ebab1334 ui: add missing keyboard function 2025-08-01 16:02:25 -07:00
Shane Smiskol 9117a414bb process replay clean up (#35878)
* format

* containers might not be set

* opts

* halves startup time for 12 procs (1.6 to 0.8s)

* stash

* Revert "stash"

This reverts commit 3e119a9602e495bd5a57b94e73fa53d4f45051b1.

* Revert "halves startup time for 12 procs (1.6 to 0.8s)"

This reverts commit a39edf0a579f0c861ccb904a2718254fe32e03d0.

* Revert "opts"

This reverts commit 4dc1f75f0909a93650f8f7e8525af3e4eae08205.

* already set!
2025-08-01 15:20:50 -07:00
DevTekVE 1966845fc9 refactor: move lateral methods from init to lateral.py (#35856)
* refactor: move lateral methods from init to lateral.py (#2594)

* Extracting lateral methods to lateral.py

* cleaning

* more cleaning

* more cleaning

* Making sure it remains where it should

* Leave rate_limit where it belongs

* Moving things to `car/controls/`

* Moving rate limit to get a taste of the changes

* clean

* copy verbatim

* clean up

* more

* now we can format

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>

* No need to change order of import

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>
Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
2025-08-01 15:17:37 -07:00
Maxime Desroches 889e386dbc ui: adapt keyboard to raylib touch api (#35875)
* key

* cancel

* more

* wow mypy very usefull as always

* _

* b

* std
2025-08-01 14:07:12 -07:00
Maxime Desroches 4e97a29e83 ui: add icon to Button (#35874)
ico
2025-08-01 12:03:22 -07:00
Adeeb Shihadeh b695715753 sensord: reset LSM (#35872)
* sensord: reset LSM

* they'll be ready in time

* switch to SW_RESET, BOOT not working for some reason
2025-08-01 10:13:39 -07:00
Jason Wen f5991caf6f params: update AthenadPid to use integer type (#35871)
* params: update `AthenadPid` to use integer type

* fix type
2025-08-01 09:29:25 -07:00
Shane Smiskol 2e4de9b7d8 process replay: speed up multi-process replay (#35867)
* holy shit

* benchmark without this main pub drain stuff

* revert

* ??

* actually this is what we want

* what is going on this is python 3.11 sir

* stash

* this is how you dew it

* minor clean up

* fix

* clean up

* clean up

* this is madness!

* typing

* clean up
2025-08-01 03:32:03 -07:00
Shane Smiskol f2c17dd688 process replay: ordered dict is in Python 2025-08-01 03:26:45 -07:00
Shane Smiskol c4298ce287 process replay: create openpilot prefix directories once (#35864)
this is so slow
2025-07-31 23:42:02 -07:00
Maxime Desroches 1de1640689 ui: improve Button widget (#35861)
* bnt

* more

* dup
2025-07-31 22:28:58 -07:00
Adeeb Shihadeh fc58c866c6 AGNOS power monitoring watchdog (#35860)
* AGNOS power monitoring watchdog

* manager should do this
2025-07-31 19:43:21 -07:00
39 changed files with 449 additions and 280 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
CI / testing:
- changed-files:
- any-glob-to-all-files: "{.github/**,**/test_*,Jenkinsfile}"
- any-glob-to-all-files: "{.github/**,**/test_*,**/test/**,Jenkinsfile}"
car:
- changed-files:
+1 -1
View File
@@ -17,7 +17,7 @@ AGNOS = TICI
Decider('MD5-timestamp')
SetOption('num_jobs', int(os.cpu_count()/2))
SetOption('num_jobs', max(1, int(os.cpu_count()/2)))
AddOption('--kaitai',
action='store_true',
@@ -1,6 +1,7 @@
import numpy as np
class Conversions:
# conversions
class CV:
# Speed
MPH_TO_KPH = 1.609344
KPH_TO_MPH = 1. / MPH_TO_KPH
@@ -17,3 +18,6 @@ class Conversions:
# Mass
LB_TO_KG = 0.453592
ACCELERATION_DUE_TO_GRAVITY = 9.81 # m/s^2
+1 -1
View File
@@ -12,7 +12,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ApiCache_Device", {PERSISTENT, STRING}},
{"ApiCache_FirehoseStats", {PERSISTENT, JSON}},
{"AssistNowToken", {PERSISTENT, STRING}},
{"AthenadPid", {PERSISTENT, STRING}},
{"AthenadPid", {PERSISTENT, INT}},
{"AthenadUploadQueue", {PERSISTENT, JSON}},
{"AthenadRecentlyViewedRoutes", {PERSISTENT, STRING}},
{"BootCount", {PERSISTENT, INT}},
+12 -6
View File
@@ -9,20 +9,19 @@ from openpilot.system.hardware.hw import Paths
from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT
class OpenpilotPrefix:
def __init__(self, prefix: str = None, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False):
def __init__(self, prefix: str = None, create_dirs_on_enter: bool = True, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False):
self.prefix = prefix if prefix else str(uuid.uuid4().hex[0:15])
self.msgq_path = os.path.join(Paths.shm_path(), self.prefix)
self.create_dirs_on_enter = create_dirs_on_enter
self.clean_dirs_on_exit = clean_dirs_on_exit
self.shared_download_cache = shared_download_cache
def __enter__(self):
self.original_prefix = os.environ.get('OPENPILOT_PREFIX', None)
os.environ['OPENPILOT_PREFIX'] = self.prefix
try:
os.mkdir(self.msgq_path)
except FileExistsError:
pass
os.makedirs(Paths.log_root(), exist_ok=True)
if self.create_dirs_on_enter:
self.create_dirs()
if self.shared_download_cache:
os.environ["COMMA_CACHE"] = DEFAULT_DOWNLOAD_CACHE_ROOT
@@ -40,6 +39,13 @@ class OpenpilotPrefix:
pass
return False
def create_dirs(self):
try:
os.mkdir(self.msgq_path)
except FileExistsError:
pass
os.makedirs(Paths.log_root(), exist_ok=True)
def clean_dirs(self):
symlink_path = Params().get_param_path()
if os.path.exists(symlink_path):
+2 -2
View File
@@ -37,9 +37,9 @@ class TestParams:
def test_params_two_things(self):
self.params.put("DongleId", "bob")
self.params.put("AthenadPid", "123")
self.params.put("AthenadPid", 123)
assert self.params.get("DongleId") == "bob"
assert self.params.get("AthenadPid") == "123"
assert self.params.get("AthenadPid") == 123
def test_params_get_block(self):
def _delayed_writer():
+1
View File
@@ -152,6 +152,7 @@ markers = [
testpaths = [
"common",
"selfdrive",
"system/manager",
"system/updated",
"system/athena",
"system/camerad",
+1 -1
View File
@@ -2,7 +2,7 @@ import math
import numpy as np
from cereal import car
from openpilot.common.conversions import Conversions as CV
from openpilot.common.constants import CV
# WARNING: this value was determined based on the model's training distribution,
+1 -1
View File
@@ -6,7 +6,7 @@ from parameterized import parameterized_class
from cereal import log
from openpilot.selfdrive.car.cruise import VCruiseHelper, V_CRUISE_MIN, V_CRUISE_MAX, V_CRUISE_INITIAL, IMPERIAL_INCREMENT
from cereal import car
from openpilot.common.conversions import Conversions as CV
from openpilot.common.constants import CV
from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver
ButtonEvent = car.CarState.ButtonEvent
+3 -3
View File
@@ -1,10 +1,10 @@
#!/usr/bin/env python3
import math
from typing import SupportsFloat
from numbers import Number
from cereal import car, log
import cereal.messaging as messaging
from openpilot.common.conversions import Conversions as CV
from openpilot.common.constants import CV
from openpilot.common.params import Params
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper
from openpilot.common.swaglog import cloudlog
@@ -127,7 +127,7 @@ class Controls:
# Ensure no NaNs/Infs
for p in ACTUATOR_FIELDS:
attr = getattr(actuators, p)
if not isinstance(attr, SupportsFloat):
if not isinstance(attr, Number):
continue
if not math.isfinite(attr):
+1 -1
View File
@@ -1,5 +1,5 @@
from cereal import log
from openpilot.common.conversions import Conversions as CV
from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL
LaneChangeState = log.LaneChangeState
+1 -1
View File
@@ -1,5 +1,5 @@
import numpy as np
from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY
from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY
from openpilot.common.realtime import DT_CTRL, DT_MDL
MIN_SPEED = 1.0
+2 -2
View File
@@ -2,9 +2,9 @@ import math
import numpy as np
from cereal import log
from opendbc.car import FRICTION_THRESHOLD, get_friction
from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction
from opendbc.car.interfaces import LatControlInputs
from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY
from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
from openpilot.common.pid import PIDController
+1 -1
View File
@@ -1,6 +1,6 @@
from cereal import log
from openpilot.common.realtime import DT_CTRL
from openpilot.common.conversions import Conversions as CV
from openpilot.common.constants import CV
CAMERA_OFFSET = 0.04
@@ -4,7 +4,7 @@ import numpy as np
import cereal.messaging as messaging
from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX
from openpilot.common.conversions import Conversions as CV
from openpilot.common.constants import CV
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.modeld.constants import ModelConstants
@@ -178,7 +178,7 @@ class LongitudinalPlanner:
def publish(self, sm, pm):
plan_send = messaging.new_message('longitudinalPlan')
plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState'])
plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState', 'radarState'])
longitudinalPlan = plan_send.longitudinalPlan
longitudinalPlan.modelMonoTime = sm.logMonoTime['modelV2']
+1 -1
View File
@@ -14,7 +14,7 @@ from typing import NoReturn
from cereal import log, car
import cereal.messaging as messaging
from openpilot.system.hardware import HARDWARE
from openpilot.common.conversions import Conversions as CV
from openpilot.common.constants import CV
from openpilot.common.params import Params
from openpilot.common.realtime import config_realtime_process
from openpilot.common.transformations.orientation import rot_from_euler, euler_from_rot
+1 -1
View File
@@ -5,7 +5,7 @@ from typing import Any
import numpy as np
from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY
from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY
from openpilot.selfdrive.locationd.models.constants import ObservationKind
from openpilot.common.swaglog import cloudlog
+1 -1
View File
@@ -4,7 +4,7 @@ from collections import deque, defaultdict
import cereal.messaging as messaging
from cereal import car, log
from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY
from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY
from openpilot.common.params import Params
from openpilot.common.realtime import config_realtime_process, DT_MDL
from openpilot.common.filter_simple import FirstOrderFilter
+1 -1
View File
@@ -7,7 +7,7 @@ from collections.abc import Callable
from cereal import log, car
import cereal.messaging as messaging
from openpilot.common.conversions import Conversions as CV
from openpilot.common.constants import CV
from openpilot.common.git import get_short_branch
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER
+19 -16
View File
@@ -43,10 +43,8 @@ SafetyModel = car.CarParams.SafetyModel
IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput)
def check_excessive_actuation(sm: messaging.SubMaster, CS: car.CarState, calibrator: PoseCalibrator, counter: int) -> tuple[int, bool]:
def check_excessive_actuation(sm: messaging.SubMaster, CS: car.CarState, calibrated_pose: Pose, counter: int) -> tuple[int, bool]:
# CS.aEgo can be noisy to bumps in the road, transitioning from standstill, losing traction, etc.
device_pose = Pose.from_live_pose(sm['livePose'])
calibrated_pose = calibrator.build_calibrated_pose(device_pose)
accel_calibrated = calibrated_pose.acceleration.x
# livePose acceleration can be noisy due to bad mounting or aliased livePose measurements
@@ -73,7 +71,9 @@ class SelfdriveD:
self.CP = CP
self.car_events = CarSpecificEvents(self.CP)
self.calibrator = PoseCalibrator()
self.pose_calibrator = PoseCalibrator()
self.calibrated_pose: Pose | None = None
# Setup sockets
self.pm = messaging.PubMaster(['selfdriveState', 'onroadEvents'])
@@ -251,12 +251,16 @@ class SelfdriveD:
# Check for excessive (longitudinal) actuation
if self.sm.updated['liveCalibration']:
self.calibrator.feed_live_calib(self.sm['liveCalibration'])
self.pose_calibrator.feed_live_calib(self.sm['liveCalibration'])
if self.sm.updated['livePose']:
device_pose = Pose.from_live_pose(self.sm['livePose'])
self.calibrated_pose = self.pose_calibrator.build_calibrated_pose(device_pose)
self.excessive_actuation_counter, excessive_actuation = check_excessive_actuation(self.sm, CS, self.calibrator, self.excessive_actuation_counter)
if not self.excessive_actuation and excessive_actuation:
set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text="longitudinal")
self.excessive_actuation = True
if self.calibrated_pose is not None:
self.excessive_actuation_counter, excessive_actuation = check_excessive_actuation(self.sm, CS, self.calibrated_pose, self.excessive_actuation_counter)
if not self.excessive_actuation and excessive_actuation:
set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text="longitudinal")
self.excessive_actuation = True
if self.excessive_actuation:
self.events.add(EventName.excessiveActuation)
@@ -312,13 +316,12 @@ class SelfdriveD:
self.events.add(EventName.cameraFrameRate)
if not REPLAY and self.rk.lagging:
self.events.add(EventName.selfdrivedLagging)
if not self.sm.valid['radarState']:
if self.sm['radarState'].radarErrors.canError:
self.events.add(EventName.canError)
elif self.sm['radarState'].radarErrors.radarUnavailableTemporary:
self.events.add(EventName.radarTempUnavailable)
else:
self.events.add(EventName.radarFault)
if self.sm['radarState'].radarErrors.canError:
self.events.add(EventName.canError)
elif self.sm['radarState'].radarErrors.radarUnavailableTemporary:
self.events.add(EventName.radarTempUnavailable)
elif any(self.sm['radarState'].radarErrors.to_dict().values()):
self.events.add(EventName.radarFault)
if not self.sm.valid['pandaStates']:
self.events.add(EventName.usbError)
if CS.canTimeout:
+69 -107
View File
@@ -4,8 +4,9 @@ import time
import copy
import heapq
import signal
from collections import Counter, OrderedDict
from collections import Counter
from dataclasses import dataclass, field
from itertools import islice
from typing import Any
from collections.abc import Callable, Iterable
from tqdm import tqdm
@@ -16,12 +17,12 @@ import cereal.messaging as messaging
from cereal import car
from cereal.services import SERVICE_LIST
from msgq.visionipc import VisionIpcServer, get_endpoint_name as vipc_get_endpoint_name
from opendbc.car.can_definitions import CanData
from opendbc.car.car_helpers import get_car, interfaces
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.common.timeout import Timeout
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.car.card import can_comm_callbacks
from openpilot.system.manager.process_config import managed_processes
from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera_state, available_streams
from openpilot.selfdrive.test.process_replay.migration import migrate_all
@@ -30,22 +31,10 @@ from openpilot.tools.lib.logreader import LogIterable
from openpilot.tools.lib.framereader import FrameReader
# Numpy gives different results based on CPU features after version 19
NUMPY_TOLERANCE = 1e-7
NUMPY_TOLERANCE = 1e-2
PROC_REPLAY_DIR = os.path.dirname(os.path.abspath(__file__))
FAKEDATA = os.path.join(PROC_REPLAY_DIR, "fakedata/")
class DummySocket:
def __init__(self):
self.data: list[bytes] = []
def receive(self, non_blocking: bool = False) -> bytes | None:
if non_blocking:
return None
return self.data.pop()
def send(self, data: bytes):
self.data.append(data)
class LauncherWithCapture:
def __init__(self, capture: ProcessOutputCapture, launcher: Callable):
@@ -63,8 +52,7 @@ class ReplayContext:
self.pubs = cfg.pubs
self.main_pub = cfg.main_pub
self.main_pub_drained = cfg.main_pub_drained
self.unlocked_pubs = cfg.unlocked_pubs
assert(len(self.pubs) != 0 or self.main_pub is not None)
assert len(self.pubs) != 0 or self.main_pub is not None
def __enter__(self):
self.open_context()
@@ -79,9 +67,8 @@ class ReplayContext:
messaging.set_fake_prefix(self.proc_name)
if self.main_pub is None:
self.events = OrderedDict()
pubs_with_events = [pub for pub in self.pubs if pub not in self.unlocked_pubs]
for pub in pubs_with_events:
self.events = {}
for pub in self.pubs:
self.events[pub] = messaging.fake_event_handle(pub, enable=True)
else:
self.events = {self.main_pub: messaging.fake_event_handle(self.main_pub, enable=True)}
@@ -138,16 +125,21 @@ class ProcessConfig:
processing_time: float = 0.001
timeout: int = 30
simulation: bool = True
# Set to service process receives on first
main_pub: str | None = None
main_pub_drained: bool = True
main_pub_drained: bool = False
vision_pubs: list[str] = field(default_factory=list)
ignore_alive_pubs: list[str] = field(default_factory=list)
unlocked_pubs: list[str] = field(default_factory=list)
def __post_init__(self):
# If the process is polling a service, we can just lock that one to speed up replay
if self.main_pub is None and isinstance(self.should_recv_callback, MessageBasedRcvCallback):
self.main_pub = self.should_recv_callback.trigger_msg_type
class ProcessContainer:
def __init__(self, cfg: ProcessConfig):
self.prefix = OpenpilotPrefix(clean_dirs_on_exit=False)
self.prefix = OpenpilotPrefix(create_dirs_on_enter=False, clean_dirs_on_exit=False)
self.cfg = copy.deepcopy(cfg)
self.process = copy.deepcopy(managed_processes[cfg.proc_name])
self.msg_queue: list[capnp._DynamicStructReader] = []
@@ -229,6 +221,7 @@ class ProcessContainer:
fingerprint: str | None, capture_output: bool
):
with self.prefix as p:
self.prefix.create_dirs()
self._setup_env(params_config, environ_config)
if self.cfg.config_callback is not None:
@@ -254,11 +247,6 @@ class ProcessContainer:
if self.cfg.init_callback is not None:
self.cfg.init_callback(self.rc, self.pm, all_msgs, fingerprint)
# wait for process to startup
with Timeout(10, error_msg=f"timed out waiting for process to start: {repr(self.cfg.proc_name)}"):
while not all(self.pm.all_readers_updated(s) for s in self.cfg.pubs if s not in self.cfg.ignore_alive_pubs):
time.sleep(0)
def stop(self):
with self.prefix:
self.process.signal(signal.SIGKILL)
@@ -267,28 +255,42 @@ class ProcessContainer:
self.prefix.clean_dirs()
self._clean_env()
def get_output_msgs(self, start_time: int):
assert self.rc and self.sockets
output_msgs = []
self.rc.wait_for_recv_called()
for socket in self.sockets:
ms = messaging.drain_sock(socket)
for m in ms:
m = m.as_builder()
m.logMonoTime = start_time + int(self.cfg.processing_time * 1e9)
output_msgs.append(m.as_reader())
return output_msgs
def run_step(self, msg: capnp._DynamicStructReader, frs: dict[str, FrameReader] | None) -> list[capnp._DynamicStructReader]:
assert self.rc and self.pm and self.sockets and self.process.proc
output_msgs = []
with self.prefix, Timeout(self.cfg.timeout, error_msg=f"timed out testing process {repr(self.cfg.proc_name)}"):
end_of_cycle = True
if self.cfg.should_recv_callback is not None:
end_of_cycle = self.cfg.should_recv_callback(msg, self.cfg, self.cnt)
self.msg_queue.append(msg)
if end_of_cycle:
self.rc.wait_for_recv_called()
end_of_cycle = True
if self.cfg.should_recv_callback is not None:
end_of_cycle = self.cfg.should_recv_callback(msg, self.cfg, self.cnt)
self.msg_queue.append(msg)
if end_of_cycle:
with self.prefix, Timeout(self.cfg.timeout, error_msg=f"timed out testing process {repr(self.cfg.proc_name)}"):
# call recv to let sub-sockets reconnect, after we know the process is ready
if self.cnt == 0:
for s in self.sockets:
messaging.recv_one_or_none(s)
# empty recv on drained pub indicates the end of messages, only do that if there're any
# certain processes use drain_sock. need to cause empty recv to break from this loop
trigger_empty_recv = False
if self.cfg.main_pub and self.cfg.main_pub_drained:
trigger_empty_recv = next((True for m in self.msg_queue if m.which() == self.cfg.main_pub), False)
trigger_empty_recv = any(m.which() == self.cfg.main_pub for m in self.msg_queue)
# get output msgs from previous inputs
output_msgs = self.get_output_msgs(msg.logMonoTime)
for m in self.msg_queue:
self.pm.send(m.which(), m.as_builder())
@@ -303,14 +305,8 @@ class ProcessContainer:
self.msg_queue = []
self.rc.unlock_sockets()
self.rc.wait_for_next_recv(trigger_empty_recv)
for socket in self.sockets:
ms = messaging.drain_sock(socket)
for m in ms:
m = m.as_builder()
m.logMonoTime = msg.logMonoTime + int(self.cfg.processing_time * 1e9)
output_msgs.append(m.as_reader())
if trigger_empty_recv:
self.rc.unlock_sockets()
self.cnt += 1
assert self.process.proc.is_alive()
@@ -320,7 +316,7 @@ class ProcessContainer:
def card_fingerprint_callback(rc, pm, msgs, fingerprint):
print("start fingerprinting")
params = Params()
canmsgs = [msg for msg in msgs if msg.which() == "can"][:300]
canmsgs = list(islice((m for m in msgs if m.which() == "can"), 300))
# card expects one arbitrary can and pandaState
rc.send_sync(pm, "can", messaging.new_message("can", 1))
@@ -344,34 +340,25 @@ def get_car_params_callback(rc, pm, msgs, fingerprint):
CarInterface = interfaces[fingerprint]
CP = CarInterface.get_non_essential_params(fingerprint)
else:
can = DummySocket()
sendcan = DummySocket()
canmsgs = [msg for msg in msgs if msg.which() == "can"]
can_msgs = ([CanData(can.address, can.dat, can.src) for can in m.can] for m in msgs if m.which() == "can")
cached_params_raw = params.get("CarParamsCache")
has_cached_cp = cached_params_raw is not None
assert len(canmsgs) != 0, "CAN messages are required for fingerprinting"
assert os.environ.get("SKIP_FW_QUERY", False) or has_cached_cp, \
assert next(can_msgs, None), "CAN messages are required for fingerprinting"
assert os.environ.get("SKIP_FW_QUERY", False) or cached_params_raw is not None, \
"CarParamsCache is required for fingerprinting. Make sure to keep carParams msgs in the logs."
for m in canmsgs[:300]:
can.send(m.as_builder().to_bytes())
can_callbacks = can_comm_callbacks(can, sendcan)
def can_recv(wait_for_one: bool = False) -> list[list[CanData]]:
return [next(can_msgs, [])]
cached_params = None
if has_cached_cp:
if cached_params_raw is not None:
with car.CarParams.from_bytes(cached_params_raw) as _cached_params:
cached_params = _cached_params
CP = get_car(*can_callbacks, lambda obd: None, Params().get_bool("AlphaLongitudinalEnabled"), False, cached_params=cached_params).CP
CP = get_car(can_recv, lambda _msgs: None, lambda obd: None, params.get_bool("AlphaLongitudinalEnabled"), False, cached_params=cached_params).CP
params.put("CarParams", CP.to_bytes())
def selfdrived_rcv_callback(msg, cfg, frame):
return (frame - 1) == 0 or msg.which() == 'carState'
def card_rcv_callback(msg, cfg, frame):
# no sendcan until card is initialized
if msg.which() != "can":
@@ -386,21 +373,6 @@ def card_rcv_callback(msg, cfg, frame):
return len(socks) > 0
def calibration_rcv_callback(msg, cfg, frame):
# calibrationd publishes 1 calibrationData every 5 cameraOdometry packets.
# should_recv always true to increment frame
return (frame - 1) == 0 or msg.which() == 'cameraOdometry'
def torqued_rcv_callback(msg, cfg, frame):
# should_recv always true to increment frame
return (frame - 1) == 0 or msg.which() == 'livePose'
def dmonitoringmodeld_rcv_callback(msg, cfg, frame):
return msg.which() == "driverCameraState"
class ModeldCameraSyncRcvCallback:
def __init__(self):
self.road_present = False
@@ -425,26 +397,13 @@ class ModeldCameraSyncRcvCallback:
class MessageBasedRcvCallback:
def __init__(self, trigger_msg_type):
def __init__(self, trigger_msg_type: str, first_frame: bool = False):
self.trigger_msg_type = trigger_msg_type
self.first_frame = first_frame
def __call__(self, msg, cfg, frame):
return msg.which() == self.trigger_msg_type
class FrequencyBasedRcvCallback:
def __init__(self, trigger_msg_type):
self.trigger_msg_type = trigger_msg_type
def __call__(self, msg, cfg, frame):
if msg.which() != self.trigger_msg_type:
return False
resp_sockets = [
s for s in cfg.subs
if frame % max(1, int(SERVICE_LIST[msg.which()].frequency / SERVICE_LIST[s].frequency)) == 0
]
return bool(len(resp_sockets))
# publish on first frame or trigger msg
return ((frame - 1) == 0 and self.first_frame) or msg.which() == self.trigger_msg_type
def selfdrived_config_callback(params, cfg, lr):
@@ -468,7 +427,7 @@ CONFIGS = [
ignore=["logMonoTime"],
config_callback=selfdrived_config_callback,
init_callback=get_car_params_callback,
should_recv_callback=selfdrived_rcv_callback,
should_recv_callback=MessageBasedRcvCallback("carState", True),
tolerance=NUMPY_TOLERANCE,
processing_time=0.004,
),
@@ -493,6 +452,7 @@ CONFIGS = [
tolerance=NUMPY_TOLERANCE,
processing_time=0.004,
main_pub="can",
main_pub_drained=True,
),
ProcessConfig(
proc_name="radard",
@@ -500,7 +460,7 @@ CONFIGS = [
subs=["radarState"],
ignore=["logMonoTime"],
init_callback=get_car_params_callback,
should_recv_callback=FrequencyBasedRcvCallback("modelV2"),
should_recv_callback=MessageBasedRcvCallback("modelV2"),
),
ProcessConfig(
proc_name="plannerd",
@@ -508,7 +468,7 @@ CONFIGS = [
subs=["longitudinalPlan", "driverAssistance"],
ignore=["logMonoTime", "longitudinalPlan.processingDelay", "longitudinalPlan.solverExecutionTime"],
init_callback=get_car_params_callback,
should_recv_callback=FrequencyBasedRcvCallback("modelV2"),
should_recv_callback=MessageBasedRcvCallback("modelV2"),
tolerance=NUMPY_TOLERANCE,
),
ProcessConfig(
@@ -517,14 +477,14 @@ CONFIGS = [
subs=["liveCalibration"],
ignore=["logMonoTime"],
init_callback=get_car_params_callback,
should_recv_callback=calibration_rcv_callback,
should_recv_callback=MessageBasedRcvCallback("cameraOdometry", True),
),
ProcessConfig(
proc_name="dmonitoringd",
pubs=["driverStateV2", "liveCalibration", "carState", "modelV2", "selfdriveState"],
subs=["driverMonitoringState"],
ignore=["logMonoTime"],
should_recv_callback=FrequencyBasedRcvCallback("driverStateV2"),
should_recv_callback=MessageBasedRcvCallback("driverStateV2"),
tolerance=NUMPY_TOLERANCE,
),
ProcessConfig(
@@ -536,7 +496,6 @@ CONFIGS = [
ignore=["logMonoTime"],
should_recv_callback=MessageBasedRcvCallback("cameraOdometry"),
tolerance=NUMPY_TOLERANCE,
unlocked_pubs=["accelerometer", "gyroscope"],
),
ProcessConfig(
proc_name="paramsd",
@@ -544,7 +503,7 @@ CONFIGS = [
subs=["liveParameters"],
ignore=["logMonoTime"],
init_callback=get_car_params_callback,
should_recv_callback=FrequencyBasedRcvCallback("livePose"),
should_recv_callback=MessageBasedRcvCallback("livePose"),
tolerance=NUMPY_TOLERANCE,
processing_time=0.004,
),
@@ -569,7 +528,7 @@ CONFIGS = [
subs=["liveTorqueParameters"],
ignore=["logMonoTime"],
init_callback=get_car_params_callback,
should_recv_callback=torqued_rcv_callback,
should_recv_callback=MessageBasedRcvCallback("livePose", True),
tolerance=NUMPY_TOLERANCE,
),
ProcessConfig(
@@ -581,7 +540,6 @@ CONFIGS = [
tolerance=NUMPY_TOLERANCE,
processing_time=0.020,
main_pub=vipc_get_endpoint_name("camerad", meta_from_camera_state("roadCameraState").stream),
main_pub_drained=False,
vision_pubs=["roadCameraState", "wideRoadCameraState"],
ignore_alive_pubs=["wideRoadCameraState"],
init_callback=get_car_params_callback,
@@ -591,11 +549,10 @@ CONFIGS = [
pubs=["liveCalibration", "driverCameraState"],
subs=["driverStateV2"],
ignore=["logMonoTime", "driverStateV2.modelExecutionTime", "driverStateV2.gpuExecutionTime"],
should_recv_callback=dmonitoringmodeld_rcv_callback,
should_recv_callback=MessageBasedRcvCallback("driverCameraState"),
tolerance=NUMPY_TOLERANCE,
processing_time=0.020,
main_pub=vipc_get_endpoint_name("camerad", meta_from_camera_state("driverCameraState").stream),
main_pub_drained=False,
vision_pubs=["driverCameraState"],
ignore_alive_pubs=["driverCameraState"],
),
@@ -703,8 +660,8 @@ def _replay_multi_process(
all_msgs = sorted(lr, key=lambda msg: msg.logMonoTime)
log_msgs = []
containers = []
try:
containers = []
for cfg in cfgs:
container = ProcessContainer(cfg)
containers.append(container)
@@ -739,6 +696,11 @@ def _replay_multi_process(
internal_pub_queue.append(m)
heapq.heappush(internal_pub_index_heap, (m.logMonoTime, len(internal_pub_queue) - 1))
log_msgs.extend(output_msgs)
# flush last set of messages from each process
for container in containers:
last_time = log_msgs[-1].logMonoTime if len(log_msgs) > 0 else int(time.monotonic() * 1e9)
log_msgs.extend(container.get_output_msgs(last_time))
finally:
for container in containers:
container.stop()
+1 -1
View File
@@ -1 +1 @@
c289a0359d1b1f26cf4d9e73a2c04b2bbfec840f
8ff1b4c9c7a34589142a07579b0051acddfe7699
+1 -1
View File
@@ -1,6 +1,6 @@
import pyray as rl
from dataclasses import dataclass
from openpilot.common.conversions import Conversions as CV
from openpilot.common.constants import CV
from openpilot.selfdrive.ui.onroad.exp_button import ExpButton
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
from openpilot.system.ui.lib.application import gui_app, FontWeight
-3
View File
@@ -10,9 +10,6 @@
// no-op base hw class
class HardwareNone {
public:
static constexpr float MAX_VOLUME = 0.7;
static constexpr float MIN_VOLUME = 0.2;
static std::string get_os_version() { return ""; }
static std::string get_name() { return ""; }
static cereal::InitData::DeviceType get_device_type() { return cereal::InitData::DeviceType::UNKNOWN; }
-2
View File
@@ -13,8 +13,6 @@
class HardwareTici : public HardwareNone {
public:
static constexpr float MAX_VOLUME = 0.9;
static constexpr float MIN_VOLUME = 0.1;
static bool TICI() { return true; }
static bool AGNOS() { return true; }
static std::string get_os_version() {
+9
View File
@@ -3,6 +3,7 @@ import datetime
import os
import signal
import sys
import time
import traceback
from cereal import log
@@ -160,6 +161,14 @@ def manager_thread() -> None:
msg.managerState.processes = [p.get_process_state_msg() for p in managed_processes.values()]
pm.send('managerState', msg)
# kick AGNOS power monitoring watchdog
try:
if sm.all_checks(['deviceState']):
with open("/var/tmp/power_watchdog", "w") as f:
f.write(str(time.monotonic()))
except Exception:
pass
# Exit main loop when uninstall/shutdown/reboot is needed
shutdown = False
for param in ("DoUninstall", "DoShutdown", "DoReboot"):
+1 -1
View File
@@ -270,7 +270,7 @@ class DaemonProcess(ManagerProcess):
stderr=open('/dev/null', 'w'),
preexec_fn=os.setpgrp)
self.params.put(self.param_name, str(proc.pid))
self.params.put(self.param_name, proc.pid)
def stop(self, retry=True, block=True, sig=None) -> None:
pass
+7
View File
@@ -98,6 +98,13 @@ def main() -> None:
(MMC5603NJ_Magn(I2C_BUS_IMU), "magnetometer", False),
]
# Reset sensors
for sensor, _, _ in sensors_cfg:
try:
sensor.reset()
except Exception:
cloudlog.exception(f"Error initializing {sensor} sensor")
# Initialize sensors
exit_event = threading.Event()
threads = [
+5
View File
@@ -40,6 +40,11 @@ class Sensor:
def device_address(self) -> int:
raise NotImplementedError
def reset(self) -> None:
# optional.
# not part of init due to shared registers
pass
def init(self) -> None:
raise NotImplementedError
+4
View File
@@ -31,6 +31,10 @@ class LSM6DS3_Accel(Sensor):
def device_address(self) -> int:
return 0x6A
def reset(self):
self.write(0x12, 0x1)
time.sleep(0.1)
def init(self):
chip_id = self.verify_chip_id(0x0F, [0x69, 0x6A])
if chip_id == 0x6A:
+4
View File
@@ -29,6 +29,10 @@ class LSM6DS3_Gyro(Sensor):
def device_address(self) -> int:
return 0x6A
def reset(self):
self.write(0x12, 0x1)
time.sleep(0.1)
def init(self):
chip_id = self.verify_chip_id(0x0F, [0x69, 0x6A])
if chip_id == 0x6A:
+64 -68
View File
@@ -11,7 +11,7 @@ from cereal import log
from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import gui_button, ButtonStyle
from openpilot.system.ui.widgets.button import Button, ButtonStyle, ButtonRadio
from openpilot.system.ui.widgets.keyboard import Keyboard
from openpilot.system.ui.widgets.label import gui_label, gui_text_box
from openpilot.system.ui.widgets.network import WifiManagerUI, WifiManagerWrapper
@@ -57,9 +57,22 @@ class Setup(Widget):
self.wifi_ui = WifiManagerUI(self.wifi_manager)
self.keyboard = Keyboard()
self.selected_radio = None
self.warning = gui_app.texture("icons/warning.png", 150, 150)
self.checkmark = gui_app.texture("icons/circled_check.png", 100, 100)
self._low_voltage_continue_button = Button("Continue", self._low_voltage_continue_button_callback)
self._low_voltage_poweroff_button = Button("Power Off", HARDWARE.shutdown)
self._getting_started_button = Button("", self._getting_started_button_callback, button_style=ButtonStyle.PRIMARY, border_radius=0)
self._software_selection_openpilot_button = ButtonRadio("openpilot", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80)
self._software_selection_custom_software_button = ButtonRadio("Custom Software", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80)
self._software_selection_continue_button = Button("Continue", self._software_selection_continue_button_callback,
button_style=ButtonStyle.PRIMARY, enabled=False)
self._software_selection_back_button = Button("Back", self._software_selection_back_button_callback)
self._download_failed_reboot_button = Button("Reboot device", HARDWARE.reboot)
self._download_failed_startover_button = Button("Start over", self._download_failed_startover_button_callback, button_style=ButtonStyle.PRIMARY)
self._network_setup_back_button = Button("Back", self._network_setup_back_button_callback)
self._network_setup_continue_button = Button("Waiting for internet", self._network_setup_continue_button_callback,
button_style=ButtonStyle.PRIMARY, enabled=False)
try:
with open("/sys/class/hwmon/hwmon1/in1_input") as f:
@@ -85,6 +98,32 @@ class Setup(Widget):
elif self.state == SetupState.DOWNLOAD_FAILED:
self.render_download_failed(rect)
def _low_voltage_continue_button_callback(self):
self.state = SetupState.GETTING_STARTED
def _getting_started_button_callback(self):
self.state = SetupState.NETWORK_SETUP
self.start_network_check()
def _software_selection_back_button_callback(self):
self.state = SetupState.NETWORK_SETUP
def _software_selection_continue_button_callback(self):
if self._software_selection_openpilot_button.selected:
self.download(OPENPILOT_URL)
else:
self.state = SetupState.CUSTOM_URL
def _download_failed_startover_button_callback(self):
self.state = SetupState.GETTING_STARTED
def _network_setup_back_button_callback(self):
self.state = SetupState.GETTING_STARTED
def _network_setup_continue_button_callback(self):
self.state = SetupState.SOFTWARE_SELECTION
self.stop_network_check_thread.set()
def render_low_voltage(self, rect: rl.Rectangle):
rl.draw_texture(self.warning, int(rect.x + 150), int(rect.y + 110), rl.WHITE)
@@ -97,11 +136,8 @@ class Setup(Widget):
button_width = (rect.width - MARGIN * 3) / 2
button_y = rect.height - MARGIN - BUTTON_HEIGHT
if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Power off"):
HARDWARE.shutdown()
if gui_button(rl.Rectangle(rect.x + MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT), "Continue"):
self.state = SetupState.GETTING_STARTED
self._low_voltage_poweroff_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT))
self._low_voltage_continue_button.render(rl.Rectangle(rect.x + MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT))
def render_getting_started(self, rect: rl.Rectangle):
title_rect = rl.Rectangle(rect.x + 165, rect.y + 280, rect.width - 265, TITLE_FONT_SIZE)
@@ -112,14 +148,10 @@ class Setup(Widget):
btn_rect = rl.Rectangle(rect.width - NEXT_BUTTON_WIDTH, 0, NEXT_BUTTON_WIDTH, rect.height)
ret = gui_button(btn_rect, "", button_style=ButtonStyle.PRIMARY, border_radius=0)
self._getting_started_button.render(btn_rect)
triangle = gui_app.texture("images/button_continue_triangle.png", 54, int(btn_rect.height))
rl.draw_texture_v(triangle, rl.Vector2(btn_rect.x + btn_rect.width / 2 - triangle.width / 2, btn_rect.height / 2 - triangle.height / 2), rl.WHITE)
if ret:
self.state = SetupState.NETWORK_SETUP
self.start_network_check()
def check_network_connectivity(self):
while not self.stop_network_check_thread.is_set():
if self.state == SetupState.NETWORK_SETUP:
@@ -157,75 +189,43 @@ class Setup(Widget):
button_width = (rect.width - BUTTON_SPACING - MARGIN * 2) / 2
button_y = rect.height - BUTTON_HEIGHT - MARGIN
if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Back"):
self.state = SetupState.GETTING_STARTED
self._network_setup_back_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT))
# Check network connectivity status
continue_enabled = self.network_connected.is_set()
self._network_setup_continue_button.enabled = continue_enabled
continue_text = ("Continue" if self.wifi_connected.is_set() else "Continue without Wi-Fi") if continue_enabled else "Waiting for internet"
if gui_button(
rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT),
continue_text,
button_style=ButtonStyle.PRIMARY if continue_enabled else ButtonStyle.NORMAL,
is_enabled=continue_enabled,
):
self.state = SetupState.SOFTWARE_SELECTION
self.stop_network_check_thread.set()
self._network_setup_continue_button._text = continue_text
self._network_setup_continue_button.render(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT))
def render_software_selection(self, rect: rl.Rectangle):
title_rect = rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE)
gui_label(title_rect, "Choose Software to Install", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM)
gui_label(title_rect, "Choose Software to Use", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM)
radio_height = 230
radio_spacing = 30
self._software_selection_continue_button.enabled = False
openpilot_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE + MARGIN * 2, rect.width - MARGIN * 2, radio_height)
openpilot_selected = self.selected_radio == "openpilot"
self._software_selection_openpilot_button.render(openpilot_rect)
rl.draw_rectangle_rounded(openpilot_rect, 0.1, 10, rl.Color(70, 91, 234, 255) if openpilot_selected else rl.Color(79, 79, 79, 255))
gui_label(rl.Rectangle(openpilot_rect.x + 100, openpilot_rect.y, openpilot_rect.width - 200, radio_height), "openpilot", BODY_FONT_SIZE)
if openpilot_selected:
checkmark_pos = rl.Vector2(openpilot_rect.x + openpilot_rect.width - 100 - self.checkmark.width,
openpilot_rect.y + radio_height / 2 - self.checkmark.height / 2)
rl.draw_texture_v(self.checkmark, checkmark_pos, rl.WHITE)
if self._software_selection_openpilot_button.selected:
self._software_selection_continue_button.enabled = True
self._software_selection_custom_software_button.selected = False
custom_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE + MARGIN * 2 + radio_height + radio_spacing, rect.width - MARGIN * 2, radio_height)
custom_selected = self.selected_radio == "custom"
self._software_selection_custom_software_button.render(custom_rect)
rl.draw_rectangle_rounded(custom_rect, 0.1, 10, rl.Color(70, 91, 234, 255) if custom_selected else rl.Color(79, 79, 79, 255))
gui_label(rl.Rectangle(custom_rect.x + 100, custom_rect.y, custom_rect.width - 200, radio_height), "Custom Software", BODY_FONT_SIZE)
if custom_selected:
checkmark_pos = rl.Vector2(custom_rect.x + custom_rect.width - 100 - self.checkmark.width, custom_rect.y + radio_height / 2 - self.checkmark.height / 2)
rl.draw_texture_v(self.checkmark, checkmark_pos, rl.WHITE)
mouse_pos = rl.get_mouse_position()
if rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT):
if rl.check_collision_point_rec(mouse_pos, openpilot_rect):
self.selected_radio = "openpilot"
elif rl.check_collision_point_rec(mouse_pos, custom_rect):
self.selected_radio = "custom"
if self._software_selection_custom_software_button.selected:
self._software_selection_continue_button.enabled = True
self._software_selection_openpilot_button.selected = False
button_width = (rect.width - BUTTON_SPACING - MARGIN * 2) / 2
button_y = rect.height - BUTTON_HEIGHT - MARGIN
if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Back"):
self.state = SetupState.NETWORK_SETUP
continue_enabled = self.selected_radio is not None
if gui_button(
rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT),
"Continue",
button_style=ButtonStyle.PRIMARY,
is_enabled=continue_enabled,
):
if continue_enabled:
if self.selected_radio == "openpilot":
self.download(OPENPILOT_URL)
else:
self.state = SetupState.CUSTOM_URL
self._software_selection_back_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT))
self._software_selection_continue_button.render(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT))
def render_downloading(self, rect: rl.Rectangle):
title_rect = rl.Rectangle(rect.x, rect.y + rect.height / 2 - TITLE_FONT_SIZE / 2, rect.width, TITLE_FONT_SIZE)
@@ -244,13 +244,8 @@ class Setup(Widget):
button_width = (rect.width - BUTTON_SPACING - MARGIN * 2) / 2
button_y = rect.height - BUTTON_HEIGHT - MARGIN
if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Reboot device"):
HARDWARE.reboot()
if gui_button(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT), "Start over",
button_style=ButtonStyle.PRIMARY):
self.state = SetupState.GETTING_STARTED
self._download_failed_reboot_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT))
self._download_failed_startover_button.render(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT))
def render_custom_url(self):
def handle_keyboard_result(result):
@@ -265,6 +260,7 @@ class Setup(Widget):
elif result == 0:
self.state = SetupState.SOFTWARE_SELECTION
self.keyboard.reset()
self.keyboard.set_title("Enter URL", "for Custom Software")
gui_app.set_modal_overlay(self.keyboard, callback=handle_keyboard_result)
+89 -9
View File
@@ -15,6 +15,8 @@ class ButtonStyle(IntEnum):
TRANSPARENT = 3 # For buttons with transparent background and border
ACTION = 4
LIST_ACTION = 5 # For list items with action buttons
NO_EFFECT = 6
KEYBOARD = 7
class TextAlignment(IntEnum):
@@ -26,6 +28,7 @@ class TextAlignment(IntEnum):
ICON_PADDING = 15
DEFAULT_BUTTON_FONT_SIZE = 60
BUTTON_DISABLED_TEXT_COLOR = rl.Color(228, 228, 228, 51)
BUTTON_DISABLED_BACKGROUND_COLOR = rl.Color(51, 51, 51, 255)
ACTION_BUTTON_FONT_SIZE = 48
BUTTON_TEXT_COLOR = {
@@ -35,6 +38,8 @@ BUTTON_TEXT_COLOR = {
ButtonStyle.TRANSPARENT: rl.BLACK,
ButtonStyle.ACTION: rl.Color(0, 0, 0, 255),
ButtonStyle.LIST_ACTION: rl.Color(228, 228, 228, 255),
ButtonStyle.NO_EFFECT: rl.Color(228, 228, 228, 255),
ButtonStyle.KEYBOARD: rl.Color(221, 221, 221, 255),
}
BUTTON_BACKGROUND_COLORS = {
@@ -44,6 +49,8 @@ BUTTON_BACKGROUND_COLORS = {
ButtonStyle.TRANSPARENT: rl.BLACK,
ButtonStyle.ACTION: rl.Color(189, 189, 189, 255),
ButtonStyle.LIST_ACTION: rl.Color(57, 57, 57, 255),
ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255),
ButtonStyle.KEYBOARD: rl.Color(68, 68, 68, 255),
}
BUTTON_PRESSED_BACKGROUND_COLORS = {
@@ -53,6 +60,8 @@ BUTTON_PRESSED_BACKGROUND_COLORS = {
ButtonStyle.TRANSPARENT: rl.BLACK,
ButtonStyle.ACTION: rl.Color(130, 130, 130, 255),
ButtonStyle.LIST_ACTION: rl.Color(74, 74, 74, 74),
ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255),
ButtonStyle.KEYBOARD: rl.Color(51, 51, 51, 255),
}
_pressed_buttons: set[str] = set() # Track mouse press state globally
@@ -162,6 +171,10 @@ class Button(Widget):
font_weight: FontWeight = FontWeight.MEDIUM,
button_style: ButtonStyle = ButtonStyle.NORMAL,
border_radius: int = 10,
text_alignment: TextAlignment = TextAlignment.CENTER,
text_padding: int = 20,
enabled: bool = True,
icon = None,
):
super().__init__()
@@ -169,27 +182,94 @@ class Button(Widget):
self._click_callback = click_callback
self._label_font = gui_app.font(FontWeight.SEMI_BOLD)
self._button_style = button_style
self._font_size = font_size
self._border_radius = border_radius
self._font_size = font_size
self._text_color = BUTTON_TEXT_COLOR[button_style]
self._background_color = BUTTON_BACKGROUND_COLORS[button_style]
self._text_size = measure_text_cached(gui_app.font(font_weight), text, font_size)
self._text_alignment = text_alignment
self._text_padding = text_padding
self._icon = icon
self.enabled = enabled
def _handle_mouse_release(self, mouse_pos: MousePos):
if self._click_callback:
print(f"Button clicked: {self._text}")
if self._click_callback and self.enabled:
self._click_callback()
def _get_background_color(self) -> rl.Color:
if self._is_pressed:
return BUTTON_PRESSED_BACKGROUND_COLORS[self._button_style]
def _update_state(self):
if self.enabled:
self._text_color = BUTTON_TEXT_COLOR[self._button_style]
if self._is_pressed:
self._background_color = BUTTON_PRESSED_BACKGROUND_COLORS[self._button_style]
else:
self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style]
else:
return BUTTON_BACKGROUND_COLORS[self._button_style]
self._background_color = BUTTON_DISABLED_BACKGROUND_COLOR
self._text_color = BUTTON_DISABLED_TEXT_COLOR
def _render(self, _):
roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2)
rl.draw_rectangle_rounded(self._rect, roundness, 10, self._get_background_color())
rl.draw_rectangle_rounded(self._rect, roundness, 10, self._background_color)
text_pos = rl.Vector2(0, self._rect.y + (self._rect.height - self._text_size.y) // 2)
text_pos.x = self._rect.x + (self._rect.width - self._text_size.x) // 2
if self._icon:
icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2
if self._text:
if self._text_alignment == TextAlignment.LEFT:
icon_x = self._rect.x + self._text_padding
text_pos.x = icon_x + self._icon.width + ICON_PADDING
elif self._text_alignment == TextAlignment.CENTER:
total_width = self._icon.width + ICON_PADDING + self._text_size.x
icon_x = self._rect.x + (self._rect.width - total_width) / 2
text_pos.x = icon_x + self._icon.width + ICON_PADDING
else:
text_pos.x = self._rect.x + self._rect.width - self._text_size.x - self._text_padding
icon_x = text_pos.x - ICON_PADDING - self._icon.width
else:
icon_x = self._rect.x + (self._rect.width - self._icon.width) / 2
rl.draw_texture_v(self._icon, rl.Vector2(icon_x, icon_y), rl.WHITE if self.enabled else rl.Color(255, 255, 255, 100))
else:
if self._text_alignment == TextAlignment.LEFT:
text_pos.x = self._rect.x + self._text_padding
elif self._text_alignment == TextAlignment.CENTER:
text_pos.x = self._rect.x + (self._rect.width - self._text_size.x) // 2
elif self._text_alignment == TextAlignment.RIGHT:
text_pos.x = self._rect.x + self._rect.width - self._text_size.x - self._text_padding
rl.draw_text_ex(self._label_font, self._text, text_pos, self._font_size, 0, self._text_color)
class ButtonRadio(Button):
def __init__(self,
text: str,
icon,
click_callback: Callable[[], None] = None,
font_size: int = DEFAULT_BUTTON_FONT_SIZE,
border_radius: int = 10,
text_padding: int = 20,
):
super().__init__(text, click_callback=click_callback, font_size=font_size, border_radius=border_radius, text_padding=text_padding, icon=icon)
self.selected = False
def _handle_mouse_release(self, mouse_pos: MousePos):
self.selected = not self.selected
if self._click_callback:
self._click_callback()
def _update_state(self):
if self.selected:
self._background_color = BUTTON_BACKGROUND_COLORS[ButtonStyle.PRIMARY]
else:
self._background_color = BUTTON_BACKGROUND_COLORS[ButtonStyle.NORMAL]
def _render(self, _):
roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2)
rl.draw_rectangle_rounded(self._rect, roundness, 10, self._background_color)
text_pos = rl.Vector2(0, self._rect.y + (self._rect.height - self._text_size.y) // 2)
text_pos.x = self._rect.x + self._text_padding
rl.draw_text_ex(self._label_font, self._text, text_pos, self._font_size, 0, self._text_color)
if self._icon and self.selected:
icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2
icon_x = self._rect.x + self._rect.width - self._icon.width - self._text_padding - ICON_PADDING
rl.draw_texture_v(self._icon, rl.Vector2(icon_x, icon_y), rl.WHITE if self.enabled else rl.Color(255, 255, 255, 100))
+59 -1
View File
@@ -1,8 +1,9 @@
import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.button import gui_button, ButtonStyle
from openpilot.system.ui.widgets.button import gui_button, ButtonStyle, Button
from openpilot.system.ui.widgets.label import gui_text_box
from openpilot.system.ui.widgets import Widget
DIALOG_WIDTH = 1520
DIALOG_HEIGHT = 600
@@ -11,6 +12,63 @@ MARGIN = 50
TEXT_AREA_HEIGHT_REDUCTION = 200
BACKGROUND_COLOR = rl.Color(27, 27, 27, 255)
class ConfirmDialog(Widget):
def __init__(self, text: str, confirm_text: str, cancel_text: str = "Cancel"):
super().__init__()
self.text = text
self._cancel_button = Button(cancel_text, self._cancel_button_callback)
self._confirm_button = Button(confirm_text, self._confirm_button_callback, button_style=ButtonStyle.PRIMARY)
self._dialog_result = DialogResult.NO_ACTION
self._cancel_text = cancel_text
def reset(self):
self._dialog_result = DialogResult.NO_ACTION
def _cancel_button_callback(self):
self._dialog_result = DialogResult.CANCEL
def _confirm_button_callback(self):
self._dialog_result = DialogResult.CONFIRM
def _render(self, rect: rl.Rectangle):
dialog_x = (gui_app.width - DIALOG_WIDTH) / 2
dialog_y = (gui_app.height - DIALOG_HEIGHT) / 2
dialog_rect = rl.Rectangle(dialog_x, dialog_y, DIALOG_WIDTH, DIALOG_HEIGHT)
bottom = dialog_rect.y + dialog_rect.height
button_width = (dialog_rect.width - 3 * MARGIN) // 2
cancel_button_x = dialog_rect.x + MARGIN
confirm_button_x = dialog_rect.x + dialog_rect.width - button_width - MARGIN
button_y = bottom - BUTTON_HEIGHT - MARGIN
cancel_button = rl.Rectangle(cancel_button_x, button_y, button_width, BUTTON_HEIGHT)
confirm_button = rl.Rectangle(confirm_button_x, button_y, button_width, BUTTON_HEIGHT)
rl.draw_rectangle_rec(dialog_rect, BACKGROUND_COLOR)
text_rect = rl.Rectangle(dialog_rect.x + MARGIN, dialog_rect.y, dialog_rect.width - 2 * MARGIN, dialog_rect.height - TEXT_AREA_HEIGHT_REDUCTION)
gui_text_box(
text_rect,
self.text,
font_size=70,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
font_weight=FontWeight.BOLD,
)
if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER):
self._dialog_result = DialogResult.CONFIRM
elif rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
self._dialog_result = DialogResult.CANCEL
if self._cancel_text:
self._confirm_button.render(confirm_button)
self._cancel_button.render(cancel_button)
else:
centered_button_x = dialog_rect.x + (dialog_rect.width - button_width) / 2
centered_confirm_button = rl.Rectangle(centered_button_x, button_y, button_width, BUTTON_HEIGHT)
self._confirm_button.render(centered_confirm_button)
return self._dialog_result
def confirm_dialog(message: str, confirm_text: str, cancel_text: str = "Cancel") -> DialogResult:
dialog_x = (gui_app.width - DIALOG_WIDTH) / 2
+52 -31
View File
@@ -1,9 +1,12 @@
from functools import partial
import time
from typing import Literal
import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import ButtonStyle, gui_button
from openpilot.system.ui.widgets.button import ButtonStyle, Button
from openpilot.system.ui.widgets.inputbox import InputBox
from openpilot.system.ui.widgets.label import gui_label
@@ -73,6 +76,11 @@ class Keyboard(Widget):
self._backspace_press_time: float = 0.0
self._backspace_last_repeat: float = 0.0
self._render_return_status = -1
self._cancel_button = Button("Cancel", self._cancel_button_callback)
self._eye_button = Button("", self._eye_button_callback, button_style=ButtonStyle.TRANSPARENT)
self._eye_open_texture = gui_app.texture("icons/eye_open.png", 81, 54)
self._eye_closed_texture = gui_app.texture("icons/eye_closed.png", 81, 54)
self._key_icons = {
@@ -83,6 +91,19 @@ class Keyboard(Widget):
ENTER_KEY: gui_app.texture("icons/arrow-right.png", 80, 80),
}
self._all_keys = {}
for l in KEYBOARD_LAYOUTS:
for _, keys in enumerate(KEYBOARD_LAYOUTS[l]):
for _, key in enumerate(keys):
if key in self._key_icons:
texture = self._key_icons[key]
self._all_keys[key] = Button("", partial(self._key_callback, key), icon=texture,
button_style=ButtonStyle.PRIMARY if key == ENTER_KEY else ButtonStyle.KEYBOARD)
else:
self._all_keys[key] = Button(key, partial(self._key_callback, key), button_style=ButtonStyle.KEYBOARD, font_size=85)
self._all_keys[CAPS_LOCK_KEY] = Button("", partial(self._key_callback, CAPS_LOCK_KEY), icon=self._key_icons[CAPS_LOCK_KEY],
button_style=ButtonStyle.KEYBOARD)
@property
def text(self):
return self._input_box.text
@@ -97,13 +118,24 @@ class Keyboard(Widget):
self._title = title
self._sub_title = sub_title
def _eye_button_callback(self):
self._password_mode = not self._password_mode
def _cancel_button_callback(self):
self.clear()
self._render_return_status = 0
def _key_callback(self, k):
if k == ENTER_KEY:
self._render_return_status = 1
else:
self.handle_key_press(k)
def _render(self, rect: rl.Rectangle):
rect = rl.Rectangle(rect.x + CONTENT_MARGIN, rect.y + CONTENT_MARGIN, rect.width - 2 * CONTENT_MARGIN, rect.height - 2 * CONTENT_MARGIN)
gui_label(rl.Rectangle(rect.x, rect.y, rect.width, 95), self._title, 90, font_weight=FontWeight.BOLD)
gui_label(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60), self._sub_title, 55, font_weight=FontWeight.NORMAL)
if gui_button(rl.Rectangle(rect.x + rect.width - 386, rect.y, 386, 125), "Cancel"):
self.clear()
return 0
self._cancel_button.render(rl.Rectangle(rect.x + rect.width - 386, rect.y, 386, 125))
# Draw input box and password toggle
input_margin = 25
@@ -111,7 +143,7 @@ class Keyboard(Widget):
self._render_input_area(input_box_rect)
# Process backspace key repeat if it's held down
if not rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT):
if not self._all_keys[BACKSPACE_KEY]._is_pressed:
self._backspace_pressed = False
if self._backspace_pressed:
@@ -146,33 +178,22 @@ class Keyboard(Widget):
start_x += new_width
is_enabled = key != ENTER_KEY or len(self._input_box.text) >= self._min_text_size
result = -1
# Check for backspace key press-and-hold
mouse_pos = rl.get_mouse_position()
mouse_over_key = rl.check_collision_point_rec(mouse_pos, key_rect)
if key == BACKSPACE_KEY and mouse_over_key:
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
self._backspace_pressed = True
self._backspace_press_time = time.monotonic()
self._backspace_last_repeat = time.monotonic()
if key == BACKSPACE_KEY and self._all_keys[BACKSPACE_KEY]._is_pressed and not self._backspace_pressed:
self._backspace_pressed = True
self._backspace_press_time = time.monotonic()
self._backspace_last_repeat = time.monotonic()
if key in self._key_icons:
if key == SHIFT_ACTIVE_KEY and self._caps_lock:
key = CAPS_LOCK_KEY
texture = self._key_icons[key]
result = gui_button(key_rect, "", icon=texture, button_style=ButtonStyle.PRIMARY if key == ENTER_KEY else ButtonStyle.NORMAL, is_enabled=is_enabled)
self._all_keys[key].enabled = is_enabled
self._all_keys[key].render(key_rect)
else:
result = gui_button(key_rect, key, KEY_FONT_SIZE, is_enabled=is_enabled)
self._all_keys[key].enabled = is_enabled
self._all_keys[key].render(key_rect)
if result:
if key == ENTER_KEY:
return 1
else:
self.handle_key_press(key)
return -1
return self._render_return_status
def _render_input_area(self, input_rect: rl.Rectangle):
if self._show_password_toggle:
@@ -183,16 +204,12 @@ class Keyboard(Widget):
eye_texture = self._eye_closed_texture if self._password_mode else self._eye_open_texture
eye_rect = rl.Rectangle(input_rect.x + input_rect.width - 90, input_rect.y, 80, input_rect.height)
self._eye_button.render(eye_rect)
eye_x = eye_rect.x + (eye_rect.width - eye_texture.width) / 2
eye_y = eye_rect.y + (eye_rect.height - eye_texture.height) / 2
rl.draw_texture_v(eye_texture, rl.Vector2(eye_x, eye_y), rl.WHITE)
# Handle click on eye icon
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT) and rl.check_collision_point_rec(
rl.get_mouse_position(), eye_rect
):
self._password_mode = not self._password_mode
else:
self._input_box.render(input_rect)
@@ -226,6 +243,10 @@ class Keyboard(Widget):
if not self._caps_lock and self._layout_name == "uppercase":
self._layout_name = "lowercase"
def reset(self):
self._render_return_status = -1
self.clear()
if __name__ == "__main__":
gui_app.init_window("Keyboard")
+23 -8
View File
@@ -1,4 +1,5 @@
from dataclasses import dataclass
from functools import partial
from threading import Lock
from typing import Literal
@@ -7,8 +8,8 @@ from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.wifi_manager import NetworkInfo, WifiManagerCallbacks, WifiManagerWrapper, SecurityType
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import ButtonStyle, gui_button
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog
from openpilot.system.ui.widgets.button import ButtonStyle, Button, TextAlignment
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.widgets.keyboard import Keyboard
from openpilot.system.ui.widgets.label import gui_label
@@ -67,8 +68,11 @@ class WifiManagerUI(Widget):
self.keyboard = Keyboard(max_text_size=MAX_PASSWORD_LENGTH, min_text_size=MIN_PASSWORD_LENGTH, show_password_toggle=True)
self._networks: list[NetworkInfo] = []
self._networks_buttons: dict[str, Button] = {}
self._forget_networks_buttons: dict[str, Button] = {}
self._lock = Lock()
self.wifi_manager = wifi_manager
self._confirm_dialog = ConfirmDialog("", "Forget", "Cancel")
self.wifi_manager.set_callbacks(
WifiManagerCallbacks(
@@ -91,10 +95,12 @@ class WifiManagerUI(Widget):
match self.state:
case StateNeedsAuth(network):
self.keyboard.set_title("Enter password", f"for {network.ssid}")
self.keyboard.reset()
gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(network, result))
case StateShowForgetConfirm(network):
gui_app.set_modal_overlay(lambda: confirm_dialog(f'Forget Wi-Fi Network "{network.ssid}"?', "Forget"),
callback=lambda result: self.on_forgot_confirm_finished(network, result))
self._confirm_dialog.text = f'Forget Wi-Fi Network "{network.ssid}"?'
self._confirm_dialog.reset()
gui_app.set_modal_overlay(self._confirm_dialog, callback=lambda result: self.on_forgot_confirm_finished(network, result))
case _:
self._draw_network_list(rect)
@@ -139,7 +145,7 @@ class WifiManagerUI(Widget):
signal_icon_rect = rl.Rectangle(rect.x + rect.width - ICON_SIZE, rect.y + (ITEM_HEIGHT - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE)
security_icon_rect = rl.Rectangle(signal_icon_rect.x - spacing - ICON_SIZE, rect.y + (ITEM_HEIGHT - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE)
gui_label(ssid_rect, network.ssid, 55)
self._networks_buttons[network.ssid].render(ssid_rect)
status_text = ""
match self.state:
@@ -162,18 +168,23 @@ class WifiManagerUI(Widget):
self.btn_width,
80,
)
if isinstance(self.state, StateIdle) and gui_button(forget_btn_rect, "Forget", button_style=ButtonStyle.ACTION) and clicked:
self.state = StateShowForgetConfirm(network)
self._forget_networks_buttons[network.ssid].render(forget_btn_rect)
self._draw_status_icon(security_icon_rect, network)
self._draw_signal_strength_icon(signal_icon_rect, network)
if isinstance(self.state, StateIdle) and rl.check_collision_point_rec(rl.get_mouse_position(), ssid_rect) and clicked:
def _networks_buttons_callback(self, network):
if self.scroll_panel.is_touch_valid():
if not network.is_saved and network.security_type != SecurityType.OPEN:
self.state = StateNeedsAuth(network)
elif not network.is_connected:
self.connect_to_network(network)
def _forget_networks_buttons_callback(self, network):
if self.scroll_panel.is_touch_valid():
if isinstance(self.state, StateIdle):
self.state = StateShowForgetConfirm(network)
def _draw_status_icon(self, rect, network: NetworkInfo):
"""Draw the status icon based on network's connection state"""
icon_file = None
@@ -211,6 +222,10 @@ class WifiManagerUI(Widget):
def _on_network_updated(self, networks: list[NetworkInfo]):
with self._lock:
self._networks = networks
for n in self._networks:
self._networks_buttons[n.ssid] = Button(n.ssid, partial(self._networks_buttons_callback, n), font_size=55, text_alignment=TextAlignment.LEFT,
button_style=ButtonStyle.NO_EFFECT)
self._forget_networks_buttons[n.ssid] = Button("Forget", partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.ACTION)
def _on_need_auth(self, ssid):
with self._lock:
+1 -1
View File
@@ -3,7 +3,7 @@ import numpy as np
from dataclasses import dataclass
from cereal import messaging, car
from opendbc.car.common.conversions import Conversions as CV
from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
+1 -2
View File
@@ -46,7 +46,7 @@ class SimulatedCar:
msg.append(self.packer.make_can_msg("SCM_BUTTONS", 0, {"CRUISE_BUTTONS": simulator_state.cruise_button}))
msg.append(self.packer.make_can_msg("GEARBOX", 0, {"GEAR": 4, "GEAR_SHIFTER": 8}))
msg.append(self.packer.make_can_msg("GEARBOX_AUTO", 0, {"GEAR_SHIFTER": 4}))
msg.append(self.packer.make_can_msg("GAS_PEDAL_2", 0, {}))
msg.append(self.packer.make_can_msg("SEATBELT_STATUS", 0, {"SEATBELT_DRIVER_LATCHED": 1}))
msg.append(self.packer.make_can_msg("STEER_STATUS", 0, {"STEER_TORQUE_SENSOR": simulator_state.user_torque}))
@@ -56,7 +56,6 @@ class SimulatedCar:
msg.append(self.packer.make_can_msg("STEER_MOTOR_TORQUE", 0, {}))
msg.append(self.packer.make_can_msg("EPB_STATUS", 0, {}))
msg.append(self.packer.make_can_msg("DOORS_STATUS", 0, {}))
msg.append(self.packer.make_can_msg("CRUISE_PARAMS", 0, {}))
msg.append(self.packer.make_can_msg("CRUISE", 0, {}))
msg.append(self.packer.make_can_msg("CRUISE_FAULT_STATUS", 0, {}))
msg.append(self.packer.make_can_msg("SCM_FEEDBACK", 0,