mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-07-23 16:32:04 +08:00
Merge branch 'master-new' into hyundai-radar-toggle
This commit is contained in:
+9
-1
@@ -1,3 +1,11 @@
|
||||
* @sunnypilot/dev-internal
|
||||
/.github/ @devtekve @sunnyhaibin
|
||||
/release/ci/ @devtekve @sunnyhaibin
|
||||
/release/ci/ @devtekve @sunnyhaibin
|
||||
/tinygrad_repo @devtekve @Discountchubbs
|
||||
/tinygrad/ @devtekve @Discountchubbs
|
||||
/selfdrive/controls/lib/longitudinal_planner.py @devtekve @Discountchubbs
|
||||
/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @devtekve @Discountchubbs
|
||||
/selfdrive/modeld/ @devtekve @Discountchubbs
|
||||
/sunnypilot/model* @devtekve @Discountchubbs
|
||||
/sunnypilot/sunnylink/ @devtekve
|
||||
/system/athena/ @devtekve
|
||||
@@ -54,8 +54,9 @@ jobs:
|
||||
git add .
|
||||
- name: update car docs
|
||||
run: |
|
||||
export PYTHONPATH="$PWD"
|
||||
scons -j$(nproc) --minimal opendbc_repo
|
||||
PYTHONPATH=. python selfdrive/car/docs.py
|
||||
python selfdrive/car/docs.py
|
||||
git add docs/CARS.md
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
|
||||
|
||||
@@ -3,7 +3,6 @@ FROM ghcr.io/commaai/openpilot-base:latest
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
ENV OPENPILOT_PATH=/home/batman/openpilot
|
||||
ENV PYTHONPATH=${OPENPILOT_PATH}:${PYTHONPATH}
|
||||
|
||||
RUN mkdir -p ${OPENPILOT_PATH}
|
||||
WORKDIR ${OPENPILOT_PATH}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
Version 0.9.10 (2025-06-30)
|
||||
========================
|
||||
|
||||
Version 0.9.9 (2025-05-23)
|
||||
========================
|
||||
* New driving model
|
||||
|
||||
+1
-1
@@ -363,13 +363,13 @@ SConscript(['rednose/SConscript'])
|
||||
|
||||
# Build system services
|
||||
SConscript([
|
||||
'system/proclogd/SConscript',
|
||||
'system/ubloxd/SConscript',
|
||||
'system/loggerd/SConscript',
|
||||
])
|
||||
if arch != "Darwin":
|
||||
SConscript([
|
||||
'system/logcatd/SConscript',
|
||||
'system/proclogd/SConscript',
|
||||
])
|
||||
|
||||
if arch == "larch64":
|
||||
|
||||
+7
-1
@@ -226,7 +226,13 @@ struct BackupManagerSP @0xf98d843bfd7004a3 {
|
||||
struct CarStateSP @0xb86e6369214c01c8 {
|
||||
}
|
||||
|
||||
struct CustomReserved8 @0xf416ec09499d9d19 {
|
||||
struct LiveMapDataSP @0xf416ec09499d9d19 {
|
||||
speedLimitValid @0 :Bool;
|
||||
speedLimit @1 :Float32;
|
||||
speedLimitAheadValid @2 :Bool;
|
||||
speedLimitAhead @3 :Float32;
|
||||
speedLimitAheadDistance @4 :Float32;
|
||||
roadName @5 :Text;
|
||||
}
|
||||
|
||||
struct CustomReserved9 @0xa1680744031fdb2d {
|
||||
|
||||
+3
-1
@@ -2281,6 +2281,7 @@ struct LiveTorqueParametersData {
|
||||
points @10 :List(List(Float32));
|
||||
version @11 :Int32;
|
||||
useParams @12 :Bool;
|
||||
calPerc @13 :Int8;
|
||||
}
|
||||
|
||||
struct LiveDelayData {
|
||||
@@ -2291,6 +2292,7 @@ struct LiveDelayData {
|
||||
lateralDelayEstimate @3 :Float32;
|
||||
lateralDelayEstimateStd @5 :Float32;
|
||||
points @4 :List(Float32);
|
||||
calPerc @6 :Int8;
|
||||
|
||||
enum Status {
|
||||
unestimated @0;
|
||||
@@ -2610,7 +2612,7 @@ struct Event {
|
||||
carControlSP @112 :Custom.CarControlSP;
|
||||
backupManagerSP @113 :Custom.BackupManagerSP;
|
||||
carStateSP @114 :Custom.CarStateSP;
|
||||
customReserved8 @115 :Custom.CustomReserved8;
|
||||
liveMapDataSP @115 :Custom.LiveMapDataSP;
|
||||
customReserved9 @116 :Custom.CustomReserved9;
|
||||
customReserved10 @136 :Custom.CustomReserved10;
|
||||
customReserved11 @137 :Custom.CustomReserved11;
|
||||
|
||||
@@ -84,6 +84,7 @@ _services: dict[str, tuple] = {
|
||||
"carParamsSP": (True, 0.02, 1),
|
||||
"carControlSP": (True, 100., 10),
|
||||
"carStateSP": (True, 100., 10),
|
||||
"liveMapDataSP": (True, 1., 1),
|
||||
|
||||
# debug
|
||||
"uiDebug": (True, 0., 1),
|
||||
|
||||
@@ -199,7 +199,6 @@ class SwagLogger(logging.Logger):
|
||||
co = f.f_code
|
||||
filename = os.path.normcase(co.co_filename)
|
||||
|
||||
# TODO: is this pylint exception correct?
|
||||
if filename == _srcfile:
|
||||
f = f.f_back
|
||||
continue
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
#define DEFAULT_MODEL "Filet o Fish (Default)"
|
||||
#define DEFAULT_MODEL "Vegetarian Filet o Fish (Default)"
|
||||
|
||||
+32
-2
@@ -71,7 +71,7 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"LastPowerDropDetected", CLEAR_ON_MANAGER_START},
|
||||
{"LastUpdateException", CLEAR_ON_MANAGER_START},
|
||||
{"LastUpdateTime", PERSISTENT},
|
||||
{"LiveDelay", PERSISTENT},
|
||||
{"LiveDelay", PERSISTENT | BACKUP},
|
||||
{"LiveParameters", PERSISTENT},
|
||||
{"LiveParametersV2", PERSISTENT},
|
||||
{"LiveTorqueParameters", PERSISTENT | DONT_LOG},
|
||||
@@ -125,15 +125,21 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"ApiCache_DriveStats", PERSISTENT},
|
||||
{"AutoLaneChangeBsmDelay", PERSISTENT},
|
||||
{"AutoLaneChangeTimer", PERSISTENT},
|
||||
{"BlinkerMinLateralControlSpeed", PERSISTENT | BACKUP},
|
||||
{"BlinkerPauseLateralControl", PERSISTENT | BACKUP},
|
||||
{"CarParamsSP", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
|
||||
{"CarParamsSPCache", CLEAR_ON_MANAGER_START},
|
||||
{"CarParamsSPPersistent", PERSISTENT},
|
||||
{"CarPlatformBundle", PERSISTENT},
|
||||
{"CustomAccIncrementsEnabled", PERSISTENT | BACKUP},
|
||||
{"CustomAccLongPressIncrement", PERSISTENT | BACKUP},
|
||||
{"CustomAccShortPressIncrement", PERSISTENT | BACKUP},
|
||||
{"DeviceBootMode", PERSISTENT | BACKUP},
|
||||
{"EnableGithubRunner", PERSISTENT | BACKUP},
|
||||
{"MaxTimeOffroad", PERSISTENT | BACKUP},
|
||||
{"Brightness", PERSISTENT | BACKUP},
|
||||
{"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION},
|
||||
{"OffroadMode", CLEAR_ON_MANAGER_START},
|
||||
{"OffroadMode_Status", CLEAR_ON_MANAGER_START},
|
||||
{"QuietMode", PERSISTENT | BACKUP},
|
||||
|
||||
// MADS params
|
||||
@@ -169,4 +175,28 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"HyundaiRadar", PERSISTENT},
|
||||
|
||||
{"DynamicExperimentalControl", PERSISTENT},
|
||||
{"BlindSpot", PERSISTENT | BACKUP},
|
||||
|
||||
// model panel params
|
||||
{"LagdToggle", PERSISTENT | BACKUP},
|
||||
|
||||
// mapd
|
||||
{"MapAdvisorySpeedLimit", CLEAR_ON_ONROAD_TRANSITION},
|
||||
{"MapdVersion", PERSISTENT},
|
||||
{"MapSpeedLimit", CLEAR_ON_ONROAD_TRANSITION},
|
||||
{"NextMapSpeedLimit", CLEAR_ON_ONROAD_TRANSITION},
|
||||
{"Offroad_OSMUpdateRequired", CLEAR_ON_MANAGER_START},
|
||||
{"OsmDbUpdatesCheck", CLEAR_ON_MANAGER_START}, // mapd database update happens with device ON, reset on boot
|
||||
{"OSMDownloadBounds", PERSISTENT},
|
||||
{"OsmDownloadedDate", PERSISTENT},
|
||||
{"OSMDownloadLocations", PERSISTENT},
|
||||
{"OSMDownloadProgress", CLEAR_ON_MANAGER_START},
|
||||
{"OsmLocal", PERSISTENT},
|
||||
{"OsmLocationName", PERSISTENT},
|
||||
{"OsmLocationTitle", PERSISTENT},
|
||||
{"OsmLocationUrl", PERSISTENT},
|
||||
{"OsmStateName", PERSISTENT},
|
||||
{"OsmStateTitle", PERSISTENT},
|
||||
{"OsmWayTest", PERSISTENT},
|
||||
{"RoadName", CLEAR_ON_ONROAD_TRANSITION},
|
||||
};
|
||||
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
import os
|
||||
import subprocess
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
|
||||
|
||||
class Spinner:
|
||||
def __init__(self):
|
||||
try:
|
||||
self.spinner_proc = subprocess.Popen(["./spinner.py"],
|
||||
stdin=subprocess.PIPE,
|
||||
cwd=os.path.join(BASEDIR, "system", "ui"),
|
||||
close_fds=True)
|
||||
except OSError:
|
||||
self.spinner_proc = None
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def update(self, spinner_text: str):
|
||||
if self.spinner_proc is not None:
|
||||
self.spinner_proc.stdin.write(spinner_text.encode('utf8') + b"\n")
|
||||
try:
|
||||
self.spinner_proc.stdin.flush()
|
||||
except BrokenPipeError:
|
||||
pass
|
||||
|
||||
def update_progress(self, cur: float, total: float):
|
||||
self.update(str(round(100 * cur / total)))
|
||||
|
||||
def close(self):
|
||||
if self.spinner_proc is not None:
|
||||
self.spinner_proc.kill()
|
||||
try:
|
||||
self.spinner_proc.communicate(timeout=2.)
|
||||
except subprocess.TimeoutExpired:
|
||||
print("WARNING: failed to kill spinner")
|
||||
self.spinner_proc = None
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import time
|
||||
with Spinner() as s:
|
||||
s.update("Spinner text")
|
||||
time.sleep(5.0)
|
||||
print("gone")
|
||||
time.sleep(5.0)
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
|
||||
|
||||
class TextWindow:
|
||||
def __init__(self, text):
|
||||
try:
|
||||
self.text_proc = subprocess.Popen(["./text.py", text],
|
||||
stdin=subprocess.PIPE,
|
||||
cwd=os.path.join(BASEDIR, "system", "ui"),
|
||||
close_fds=True)
|
||||
except OSError:
|
||||
self.text_proc = None
|
||||
|
||||
def get_status(self):
|
||||
if self.text_proc is not None:
|
||||
self.text_proc.poll()
|
||||
return self.text_proc.returncode
|
||||
return None
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def close(self):
|
||||
if self.text_proc is not None:
|
||||
self.text_proc.terminate()
|
||||
self.text_proc = None
|
||||
|
||||
def wait_for_exit(self):
|
||||
if self.text_proc is not None:
|
||||
while True:
|
||||
if self.get_status() == 1:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
text = """Traceback (most recent call last):
|
||||
File "./controlsd.py", line 608, in <module>
|
||||
main()
|
||||
File "./controlsd.py", line 604, in main
|
||||
controlsd_thread(sm, pm, logcan)
|
||||
File "./controlsd.py", line 455, in controlsd_thread
|
||||
1/0
|
||||
ZeroDivisionError: division by zero"""
|
||||
print(text)
|
||||
|
||||
with TextWindow(text) as s:
|
||||
for _ in range(100):
|
||||
if s.get_status() == 1:
|
||||
print("Got exit button")
|
||||
break
|
||||
time.sleep(0.1)
|
||||
print("gone")
|
||||
+1
-1
@@ -1 +1 @@
|
||||
#define COMMA_VERSION "0.9.9"
|
||||
#define COMMA_VERSION "0.9.10"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import os
|
||||
import time
|
||||
import struct
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
|
||||
WATCHDOG_FN = f"{Paths.shm_path()}/wd_"
|
||||
_LAST_KICK = 0.0
|
||||
|
||||
def kick_watchdog():
|
||||
global _LAST_KICK
|
||||
current_time = time.monotonic()
|
||||
|
||||
if current_time - _LAST_KICK < 1.0:
|
||||
return
|
||||
|
||||
try:
|
||||
with open(f"{WATCHDOG_FN}{os.getpid()}", 'wb') as f:
|
||||
f.write(struct.pack('<Q', int(current_time * 1e9)))
|
||||
f.flush()
|
||||
_LAST_KICK = current_time
|
||||
except OSError:
|
||||
pass
|
||||
+8
-8
@@ -15,7 +15,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=A3 2014-19">Buy Here</a></sub></details>|||
|
||||
|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=A3 Sportback e-tron 2017-18">Buy Here</a></sub></details>|||
|
||||
|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=Q2 2018">Buy Here</a></sub></details>|||
|
||||
|Audi|Q3 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=Q3 2019-23">Buy Here</a></sub></details>|||
|
||||
|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=Q3 2019-24">Buy Here</a></sub></details>|||
|
||||
|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=RS3 2018">Buy Here</a></sub></details>|||
|
||||
|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=S3 2015-17">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
@@ -38,6 +38,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape Hybrid 2023-24">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|
||||
|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape Plug-in Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||
|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape Plug-in Hybrid 2023-24">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|
||||
|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Expedition 2022-24">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=MewJc9LYp9M|
|
||||
|Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Explorer 2020-24">Buy Here</a></sub></details>|||
|
||||
|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Explorer Hybrid 2020-24">Buy Here</a></sub></details>|||
|
||||
|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=F-150 2021-23">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=MewJc9LYp9M|
|
||||
@@ -53,7 +54,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Ford|Maverick 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick 2023-24">Buy Here</a></sub></details>|||
|
||||
|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick Hybrid 2022">Buy Here</a></sub></details>|||
|
||||
|Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick Hybrid 2023-24">Buy Here</a></sub></details>|||
|
||||
|Ford|Mustang Mach-E 2021-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Mustang Mach-E 2021-23">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|
||||
|Ford|Mustang Mach-E 2021-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Mustang Mach-E 2021-24">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|
||||
|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Ranger 2024">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|
||||
|Genesis|G70 2018|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G70 2018">Buy Here</a></sub></details>|||
|
||||
|Genesis|G70 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G70 2019-21">Buy Here</a></sub></details>|||
|
||||
@@ -77,8 +78,8 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Honda|Civic 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic Hatchback 2017-21|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback 2017-21">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic Hatchback Hybrid 2023 (Europe only)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback Hybrid 2023 (Europe only)">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback Hybrid 2025|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback Hybrid 2025">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback Hybrid (Europe only) 2023">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V 2015-16|Touring Trim|openpilot|26 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V 2015-16">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V 2017-22">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V Hybrid 2017-22">Buy Here</a></sub></details>|||
|
||||
@@ -115,7 +116,6 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Hyundai|Ioniq Plug-in Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|32 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Ioniq Plug-in Hybrid 2019">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Ioniq Plug-in Hybrid 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Ioniq Plug-in Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona 2020|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|6 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona 2020">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona 2022|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai O connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona 2022">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona Electric 2018-21">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai O connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona Electric 2022-23">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona Electric (with HDA II, Korea only) 2023[<sup>6</sup>](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai R connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona Electric (with HDA II, Korea only) 2023">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=U2fOCmcQ8hw" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
@@ -181,12 +181,12 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Kia|Telluride 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Kia&model=Telluride 2020-22">Buy Here</a></sub></details>|||
|
||||
|Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=CT Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=ES 2017-18">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES 2019-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=ES 2019-24">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=ES 2019-25">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES Hybrid 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=ES Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=ES Hybrid 2019-25">Buy Here</a></sub></details>|<a href="https://youtu.be/BZ29osRVJeg?t=12" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Lexus|GS F 2016|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=GS F 2016">Buy Here</a></sub></details>|||
|
||||
|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=IS 2017-19">Buy Here</a></sub></details>|||
|
||||
|Lexus|IS 2022-23|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=IS 2022-23">Buy Here</a></sub></details>|||
|
||||
|Lexus|IS 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=IS 2022-24">Buy Here</a></sub></details>|||
|
||||
|Lexus|LC 2024|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=LC 2024">Buy Here</a></sub></details>|||
|
||||
|Lexus|NX 2018-19|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX 2018-19">Buy Here</a></sub></details>|||
|
||||
|Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX 2020-21">Buy Here</a></sub></details>|||
|
||||
@@ -313,11 +313,11 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Polo GTI 2018-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
||||
|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=T-Cross 2021">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
||||
|Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=T-Roc 2018-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Taos 2022-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Taos 2022-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Taos 2022-24">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Teramont 2018-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Teramont Cross Sport 2021-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Teramont X 2021-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Tiguan 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Tiguan 2018-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Tiguan 2018-24">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Tiguan eHybrid 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Touran 2016-23">Buy Here</a></sub></details>|||
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ Development is coordinated through [Discord](https://discord.comma.ai) and GitHu
|
||||
### Getting Started
|
||||
|
||||
* Setup your [development environment](../tools/)
|
||||
* Read about the [development workflow](WORKFLOW.md)
|
||||
* Join our [Discord](https://discord.comma.ai)
|
||||
* Docs are at https://docs.comma.ai and https://blog.comma.ai
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# openpilot development workflow
|
||||
|
||||
Aside from the ML models, most tools used for openpilot development are in this repo.
|
||||
|
||||
Most development happens on normal Ubuntu workstations, and not in cars or directly on comma devices. See the [setup guide](../tools) for getting your PC setup for openpilot development.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# get the latest stuff
|
||||
git pull
|
||||
git lfs pull
|
||||
git submodule update --init --recursive
|
||||
|
||||
# update dependencies
|
||||
tools/ubuntu_setup.sh
|
||||
|
||||
# build everything
|
||||
scons -j$(nproc)
|
||||
|
||||
# build just the ui with either of these
|
||||
scons -j8 selfdrive/ui/
|
||||
cd selfdrive/ui/ && scons -u -j8
|
||||
|
||||
# test everything
|
||||
pytest
|
||||
|
||||
# test just logging services
|
||||
cd system/loggerd && pytest .
|
||||
|
||||
# run the linter
|
||||
op lint
|
||||
```
|
||||
+1
-1
@@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1
|
||||
export VECLIB_MAXIMUM_THREADS=1
|
||||
|
||||
if [ -z "$AGNOS_VERSION" ]; then
|
||||
export AGNOS_VERSION="12.3"
|
||||
export AGNOS_VERSION="12.4"
|
||||
fi
|
||||
|
||||
export STAGING_ROOT="/data/safe_staging"
|
||||
|
||||
+1
-1
Submodule panda updated: 5f4742c39e...c33cfa0803
+7
-2
@@ -68,6 +68,9 @@ dependencies = [
|
||||
|
||||
# logreader
|
||||
"zstandard",
|
||||
|
||||
# ui
|
||||
"qrcode",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
@@ -85,7 +88,8 @@ testing = [
|
||||
"pytest-cov",
|
||||
"pytest-cpp",
|
||||
"pytest-subtests",
|
||||
"pytest-xdist",
|
||||
# https://github.com/pytest-dev/pytest-xdist/issues/1215
|
||||
"pytest-xdist @ git+https://github.com/sshane/pytest-xdist@909e97b49d12401c10608f9d777bfc9dab8a4413",
|
||||
"pytest-timeout",
|
||||
"pytest-randomly",
|
||||
"pytest-asyncio",
|
||||
@@ -102,8 +106,8 @@ dev = [
|
||||
"azure-storage-blob",
|
||||
"dbus-next",
|
||||
"dictdiffer",
|
||||
"lru-dict",
|
||||
"matplotlib",
|
||||
"opencv-python-headless",
|
||||
"parameterized >=0.8, <0.9",
|
||||
"pyautogui",
|
||||
"pygame",
|
||||
@@ -260,6 +264,7 @@ lint.flake8-implicit-str-concat.allow-multiline = false
|
||||
"tools".msg = "Use openpilot.tools"
|
||||
"pytest.main".msg = "pytest.main requires special handling that is easy to mess up!"
|
||||
"unittest".msg = "Use pytest"
|
||||
"pyray.measure_text_ex".msg = "Use openpilot.system.ui.lib.text_measure"
|
||||
|
||||
[tool.coverage.run]
|
||||
concurrency = ["multiprocessing", "thread"]
|
||||
|
||||
+1
-1
Submodule rednose_repo updated: 8b86052919...7fddc8e6d4
+15
-20
@@ -1,36 +1,31 @@
|
||||
# openpilot releases
|
||||
|
||||
```
|
||||
## release checklist
|
||||
|
||||
**Go to `devel-staging`**
|
||||
- [ ] update RELEASES.md
|
||||
- [ ] update `devel-staging`: `git reset --hard origin/master-ci`
|
||||
- [ ] open a pull request from `devel-staging` to `devel`
|
||||
- [ ] post on Discord
|
||||
|
||||
**Go to `devel`**
|
||||
- [ ] update RELEASES.md
|
||||
- [ ] close out milestone
|
||||
- [ ] post on Discord dev channel
|
||||
- [ ] bump version on master: `common/version.h` and `RELEASES.md`
|
||||
- [ ] merge the pull request
|
||||
|
||||
tests:
|
||||
- [ ] update from previous release -> new release
|
||||
- [ ] update from new release -> previous release
|
||||
- [ ] fresh install with `openpilot-test.comma.ai`
|
||||
- [ ] drive on fresh install
|
||||
- [ ] comma body test
|
||||
- [ ] no submodules or LFS
|
||||
- [ ] check sentry, MTBF, etc.
|
||||
- [ ] before merging the pull request
|
||||
- [ ] update from previous release -> new release
|
||||
- [ ] update from new release -> previous release
|
||||
- [ ] fresh install with `openpilot-test.comma.ai`
|
||||
- [ ] drive on fresh install
|
||||
- [ ] no submodules or LFS
|
||||
- [ ] check sentry, MTBF, etc.
|
||||
|
||||
**Go to `release3`**
|
||||
- [ ] publish the blog post
|
||||
- [ ] `git reset --hard origin/release3-staging`
|
||||
- [ ] tag the release
|
||||
```
|
||||
git tag v0.X.X <commit-hash>
|
||||
git push origin v0.X.X
|
||||
```
|
||||
- [ ] tag the release: `git tag v0.X.X <commit-hash> && git push origin v0.X.X`
|
||||
- [ ] create GitHub release
|
||||
- [ ] final test install on `openpilot.comma.ai`
|
||||
- [ ] update production
|
||||
- [ ] Post on Discord, X, etc.
|
||||
- [ ] update factory provisioning
|
||||
- [ ] close out milestone
|
||||
- [ ] post on Discord, X, etc.
|
||||
```
|
||||
|
||||
@@ -12,11 +12,10 @@
|
||||
|
||||
<h5>Quectel/EG25-G</h5>
|
||||
<p>FCC ID: XMR201903EG25G</p>
|
||||
<p>
|
||||
This device complies with Part 15 of the FCC Rules.
|
||||
Operation is subject to the following two conditions:
|
||||
<p>This device complies with Part 15 of the FCC Rules.</p>
|
||||
<p>Operation is subject to the following two conditions:</p>
|
||||
|
||||
<p>(1) this device may not cause harmful interference, and
|
||||
<p>(1) this device may not cause harmful interference, and</p>
|
||||
<p>(2) this device must accept any interference received, including interference that may cause undesired operation.</p>
|
||||
|
||||
The following test reports are subject to this declaration:
|
||||
|
||||
@@ -307,6 +307,7 @@ class Car:
|
||||
|
||||
# sunnypilot
|
||||
self.dynamic_experimental_control = self.params.get_bool("DynamicExperimentalControl")
|
||||
self.v_cruise_helper.read_custom_set_speed_params()
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import numpy as np
|
||||
|
||||
from cereal import car
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.sunnypilot.selfdrive.car.cruise_ext import VCruiseHelperSP
|
||||
|
||||
|
||||
# WARNING: this value was determined based on the model's training distribution,
|
||||
@@ -28,8 +29,9 @@ CRUISE_INTERVAL_SIGN = {
|
||||
}
|
||||
|
||||
|
||||
class VCruiseHelper:
|
||||
class VCruiseHelper(VCruiseHelperSP):
|
||||
def __init__(self, CP):
|
||||
VCruiseHelperSP.__init__(self)
|
||||
self.CP = CP
|
||||
self.v_cruise_kph = V_CRUISE_UNSET
|
||||
self.v_cruise_cluster_kph = V_CRUISE_UNSET
|
||||
@@ -99,7 +101,7 @@ class VCruiseHelper:
|
||||
if not self.button_change_states[button_type]["enabled"]:
|
||||
return
|
||||
|
||||
v_cruise_delta = v_cruise_delta * (5 if long_press else 1)
|
||||
long_press, v_cruise_delta = VCruiseHelperSP.update_v_cruise_delta(self, long_press, v_cruise_delta)
|
||||
if long_press and self.v_cruise_kph % v_cruise_delta != 0: # partial interval
|
||||
self.v_cruise_kph = CRUISE_NEAREST_FUNC[button_type](self.v_cruise_kph / v_cruise_delta) * v_cruise_delta
|
||||
else:
|
||||
|
||||
@@ -44,7 +44,7 @@ class Controls(ControlsExt):
|
||||
|
||||
self.sm = messaging.SubMaster(['liveParameters', 'liveTorqueParameters', 'modelV2', 'selfdriveState',
|
||||
'liveCalibration', 'livePose', 'longitudinalPlan', 'carState', 'carOutput',
|
||||
'driverMonitoringState', 'onroadEvents', 'driverAssistance'] + self.sm_services_ext,
|
||||
'driverMonitoringState', 'onroadEvents', 'driverAssistance', 'liveDelay'] + self.sm_services_ext,
|
||||
poll='selfdriveState')
|
||||
self.pm = messaging.PubMaster(['carControl', 'controlsState'] + self.pm_services_ext)
|
||||
|
||||
@@ -93,6 +93,7 @@ class Controls(ControlsExt):
|
||||
torque_params.frictionCoefficientFiltered)
|
||||
|
||||
self.LaC.extension.update_model_v2(self.sm['modelV2'])
|
||||
self.LaC.extension.update_lateral_lag(self.sm['liveDelay'].lateralDelay)
|
||||
|
||||
long_plan = self.sm['longitudinalPlan']
|
||||
model_v2 = self.sm['modelV2']
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import jinja2
|
||||
import os
|
||||
|
||||
from cereal import car
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from opendbc.car.interfaces import get_interface_attr
|
||||
|
||||
Ecu = car.CarParams.Ecu
|
||||
|
||||
CARS = get_interface_attr('CAR')
|
||||
FW_VERSIONS = get_interface_attr('FW_VERSIONS')
|
||||
FINGERPRINTS = get_interface_attr('FINGERPRINTS')
|
||||
ECU_NAME = {v: k for k, v in Ecu.schema.enumerants.items()}
|
||||
|
||||
FINGERPRINTS_PY_TEMPLATE = jinja2.Template("""
|
||||
{%- if FINGERPRINTS[brand] %}
|
||||
# ruff: noqa: E501
|
||||
{% endif %}
|
||||
{% if FW_VERSIONS[brand] %}
|
||||
from opendbc.car.structs import CarParams
|
||||
{% endif %}
|
||||
from opendbc.car.{{brand}}.values import CAR
|
||||
{% if FW_VERSIONS[brand] %}
|
||||
|
||||
Ecu = CarParams.Ecu
|
||||
{% endif %}
|
||||
{% if comments +%}
|
||||
{{ comments | join() }}
|
||||
{% endif %}
|
||||
{% if FINGERPRINTS[brand] %}
|
||||
|
||||
FINGERPRINTS = {
|
||||
{% for car, fingerprints in FINGERPRINTS[brand].items() %}
|
||||
CAR.{{car.name}}: [{
|
||||
{% for fingerprint in fingerprints %}
|
||||
{% if not loop.first %}
|
||||
{{ "{" }}
|
||||
{% endif %}
|
||||
{% for key, value in fingerprint.items() %}{{key}}: {{value}}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||
|
||||
}{% if loop.last %}]{% endif %},
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
FW_VERSIONS{% if not FW_VERSIONS[brand] %}: dict[str, dict[tuple, list[bytes]]]{% endif %} = {
|
||||
{% for car, _ in FW_VERSIONS[brand].items() %}
|
||||
CAR.{{car.name}}: {
|
||||
{% for key, fw_versions in FW_VERSIONS[brand][car].items() %}
|
||||
(Ecu.{{ECU_NAME[key[0]]}}, 0x{{"%0x" | format(key[1] | int)}}, \
|
||||
{% if key[2] %}0x{{"%0x" | format(key[2] | int)}}{% else %}{{key[2]}}{% endif %}): [
|
||||
{% for fw_version in (fw_versions + extra_fw_versions.get(car, {}).get(key, [])) | unique | sort %}
|
||||
{{fw_version}},
|
||||
{% endfor %}
|
||||
],
|
||||
{% endfor %}
|
||||
},
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
""", trim_blocks=True)
|
||||
|
||||
|
||||
def format_brand_fw_versions(brand, extra_fw_versions: None | dict[str, dict[tuple, list[bytes]]] = None):
|
||||
extra_fw_versions = extra_fw_versions or {}
|
||||
|
||||
fingerprints_file = os.path.join(BASEDIR, f"opendbc/car/{brand}/fingerprints.py")
|
||||
with open(fingerprints_file) as f:
|
||||
comments = [line for line in f.readlines() if line.startswith("#") and "noqa" not in line]
|
||||
|
||||
with open(fingerprints_file, "w") as f:
|
||||
f.write(FINGERPRINTS_PY_TEMPLATE.render(brand=brand, comments=comments, ECU_NAME=ECU_NAME,
|
||||
FINGERPRINTS=FINGERPRINTS, FW_VERSIONS=FW_VERSIONS,
|
||||
extra_fw_versions=extra_fw_versions))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for brand in FW_VERSIONS.keys():
|
||||
format_brand_fw_versions(brand)
|
||||
@@ -13,6 +13,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.params import Params
|
||||
from openpilot.common.realtime import config_realtime_process
|
||||
@@ -36,8 +37,11 @@ RPY_INIT = np.array([0.0,0.0,0.0])
|
||||
WIDE_FROM_DEVICE_EULER_INIT = np.array([0.0, 0.0, 0.0])
|
||||
HEIGHT_INIT = np.array([1.22])
|
||||
|
||||
# These values are needed to accommodate the model frame in the narrow cam of the C3
|
||||
PITCH_LIMITS = np.array([-0.09074112085129739, 0.17])
|
||||
# These values are needed to accommodate the model frame in the narrow cam
|
||||
if HARDWARE.get_device_type() == 'mici':
|
||||
PITCH_LIMITS = np.array([-0.143101, 0.22235988])
|
||||
else:
|
||||
PITCH_LIMITS = np.array([-0.09074112085129739, 0.17])
|
||||
YAW_LIMITS = np.array([-0.06912048084718224, 0.06912048084718235])
|
||||
DEBUG = os.getenv("DEBUG") is not None
|
||||
|
||||
|
||||
@@ -82,6 +82,12 @@ class PointBuckets:
|
||||
total_points_valid = self.__len__() >= self.min_points_total
|
||||
return individual_buckets_valid and total_points_valid
|
||||
|
||||
def get_valid_percent(self) -> int:
|
||||
total_points_perc = min(self.__len__() / self.min_points_total * 100, 100)
|
||||
individual_buckets_perc = min(min(len(v) / min_pts * 100 for v, min_pts in
|
||||
zip(self.buckets.values(), self.buckets_min_points.values(), strict=True)), 100)
|
||||
return int((total_points_perc + individual_buckets_perc) / 2)
|
||||
|
||||
def is_calculable(self) -> bool:
|
||||
return all(len(v) > 0 for v in self.buckets.values())
|
||||
|
||||
|
||||
@@ -157,7 +157,8 @@ class LateralLagEstimator:
|
||||
block_count: int = BLOCK_NUM, min_valid_block_count: int = BLOCK_NUM_NEEDED, block_size: int = BLOCK_SIZE,
|
||||
window_sec: float = MOVING_WINDOW_SEC, okay_window_sec: float = MIN_OKAY_WINDOW_SEC, min_recovery_buffer_sec: float = MIN_RECOVERY_BUFFER_SEC,
|
||||
min_vego: float = MIN_VEGO, min_yr: float = MIN_ABS_YAW_RATE, min_ncc: float = MIN_NCC,
|
||||
max_lat_accel: float = MAX_LAT_ACCEL, max_lat_accel_diff: float = MAX_LAT_ACCEL_DIFF, min_confidence: float = MIN_CONFIDENCE):
|
||||
max_lat_accel: float = MAX_LAT_ACCEL, max_lat_accel_diff: float = MAX_LAT_ACCEL_DIFF, min_confidence: float = MIN_CONFIDENCE,
|
||||
enabled: bool = True):
|
||||
self.dt = dt
|
||||
self.window_sec = window_sec
|
||||
self.okay_window_sec = okay_window_sec
|
||||
@@ -172,6 +173,7 @@ class LateralLagEstimator:
|
||||
self.min_confidence = min_confidence
|
||||
self.max_lat_accel = max_lat_accel
|
||||
self.max_lat_accel_diff = max_lat_accel_diff
|
||||
self.enabled = enabled
|
||||
|
||||
self.t = 0.0
|
||||
self.lat_active = False
|
||||
@@ -206,7 +208,7 @@ class LateralLagEstimator:
|
||||
liveDelay = msg.liveDelay
|
||||
|
||||
valid_mean_lag, valid_std, current_mean_lag, current_std = self.block_avg.get()
|
||||
if self.block_avg.valid_blocks >= self.min_valid_block_count and not np.isnan(valid_mean_lag) and not np.isnan(valid_std):
|
||||
if self.enabled and self.block_avg.valid_blocks >= self.min_valid_block_count and not np.isnan(valid_mean_lag) and not np.isnan(valid_std):
|
||||
if valid_std > MAX_LAG_STD:
|
||||
liveDelay.status = log.LiveDelayData.Status.invalid
|
||||
else:
|
||||
@@ -227,6 +229,8 @@ class LateralLagEstimator:
|
||||
liveDelay.lateralDelayEstimateStd = 0.0
|
||||
|
||||
liveDelay.validBlocks = self.block_avg.valid_blocks
|
||||
liveDelay.calPerc = min(100 * (self.block_avg.valid_blocks * self.block_size + self.block_avg.idx) //
|
||||
(self.min_valid_block_count * self.block_size), 100)
|
||||
if debug:
|
||||
liveDelay.points = self.block_avg.values.flatten().tolist()
|
||||
|
||||
@@ -303,7 +307,8 @@ class LateralLagEstimator:
|
||||
self.block_avg.update(delay)
|
||||
self.last_estimate_t = self.t
|
||||
|
||||
def actuator_delay(self, expected_sig: np.ndarray, actual_sig: np.ndarray, mask: np.ndarray, dt: float, max_lag: float) -> tuple[float, float, float]:
|
||||
@staticmethod
|
||||
def actuator_delay(expected_sig: np.ndarray, actual_sig: np.ndarray, mask: np.ndarray, dt: float, max_lag: float) -> tuple[float, float, float]:
|
||||
assert len(expected_sig) == len(actual_sig)
|
||||
max_lag_samples = int(max_lag / dt)
|
||||
padded_size = fft_next_good_size(len(expected_sig) + max_lag_samples)
|
||||
@@ -366,7 +371,10 @@ def main():
|
||||
params = Params()
|
||||
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
|
||||
|
||||
lag_learner = LateralLagEstimator(CP, 1. / SERVICE_LIST['livePose'].frequency)
|
||||
# TODO: remove me, lagd is in shadow mode on release
|
||||
is_release = params.get_bool("IsReleaseBranch")
|
||||
|
||||
lag_learner = LateralLagEstimator(CP, 1. / SERVICE_LIST['livePose'].frequency, enabled=not is_release)
|
||||
if (initial_lag_params := retrieve_initial_lag(params, CP)) is not None:
|
||||
lag, valid_blocks = initial_lag_params
|
||||
lag_learner.reset(lag, valid_blocks)
|
||||
|
||||
@@ -3,7 +3,7 @@ import numpy as np
|
||||
import time
|
||||
import pytest
|
||||
|
||||
from cereal import messaging, log
|
||||
from cereal import messaging, log, car
|
||||
from openpilot.selfdrive.locationd.lagd import LateralLagEstimator, retrieve_initial_lag, masked_normalized_cross_correlation, \
|
||||
BLOCK_NUM_NEEDED, BLOCK_SIZE, MIN_OKAY_WINDOW_SEC
|
||||
from openpilot.selfdrive.test.process_replay.migration import migrate, migrate_carParams
|
||||
@@ -16,11 +16,7 @@ MAX_ERR_FRAMES = 1
|
||||
DT = 0.05
|
||||
|
||||
|
||||
def process_messages(mocker, estimator, lag_frames, n_frames, vego=20.0, rejection_threshold=0.0):
|
||||
class ZeroMock(mocker.Mock):
|
||||
def __getattr__(self, *args):
|
||||
return 0
|
||||
|
||||
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
|
||||
@@ -31,19 +27,15 @@ def process_messages(mocker, estimator, lag_frames, n_frames, vego=20.0, rejecti
|
||||
if rejected:
|
||||
actual_la = desired_la
|
||||
|
||||
desired_cuvature = desired_la / (vego ** 2)
|
||||
actual_yr = actual_la / vego
|
||||
desired_cuvature = float(desired_la / (vego ** 2))
|
||||
actual_yr = float(actual_la / vego)
|
||||
msgs = [
|
||||
(t, "carControl", mocker.Mock(latActive=not rejected)),
|
||||
(t, "carState", mocker.Mock(vEgo=vego, steeringPressed=False)),
|
||||
(t, "controlsState", mocker.Mock(desiredCurvature=desired_cuvature,
|
||||
lateralControlState=mocker.Mock(which=mocker.Mock(return_value='debugControlState'), debugControlState=ZeroMock()))),
|
||||
(t, "livePose", mocker.Mock(orientationNED=ZeroMock(),
|
||||
velocityDevice=ZeroMock(),
|
||||
accelerationDevice=ZeroMock(),
|
||||
angularVelocityDevice=ZeroMock(z=actual_yr, valid=True),
|
||||
posenetOK=True, inputsOK=True)),
|
||||
(t, "liveCalibration", mocker.Mock(rpyCalib=[0, 0, 0], calStatus=log.LiveCalibrationData.Status.calibrated)),
|
||||
(t, "carControl", car.CarControl(latActive=not rejected)),
|
||||
(t, "carState", car.CarState(vEgo=vego, steeringPressed=False)),
|
||||
(t, "controlsState", log.ControlsState(desiredCurvature=desired_cuvature)),
|
||||
(t, "livePose", log.LivePose(angularVelocityDevice=log.LivePose.XYZMeasurement(z=actual_yr, valid=True),
|
||||
posenetOK=True, inputsOK=True)),
|
||||
(t, "liveCalibration", log.LiveCalibrationData(rpyCalib=[0, 0, 0], calStatus=log.LiveCalibrationData.Status.calibrated)),
|
||||
]
|
||||
for t, w, m in msgs:
|
||||
estimator.handle_log(t, w, m)
|
||||
@@ -94,40 +86,56 @@ class TestLagd:
|
||||
corr = masked_normalized_cross_correlation(desired_sig, actual_sig, mask, 200)[len(desired_sig) - 1:len(desired_sig) + 20]
|
||||
assert np.argmax(corr) in range(lag_frames - MAX_ERR_FRAMES, lag_frames + MAX_ERR_FRAMES + 1)
|
||||
|
||||
def test_empty_estimator(self, mocker):
|
||||
mocked_CP = mocker.Mock(steerActuatorDelay=0.8)
|
||||
def test_empty_estimator(self):
|
||||
mocked_CP = car.CarParams(steerActuatorDelay=0.8)
|
||||
estimator = LateralLagEstimator(mocked_CP, DT)
|
||||
msg = estimator.get_msg(True)
|
||||
assert msg.liveDelay.status == 'unestimated'
|
||||
assert np.allclose(msg.liveDelay.lateralDelay, estimator.initial_lag)
|
||||
assert np.allclose(msg.liveDelay.lateralDelayEstimate, estimator.initial_lag)
|
||||
assert msg.liveDelay.validBlocks == 0
|
||||
assert msg.liveDelay.calPerc == 0
|
||||
|
||||
def test_estimator_basics(self, mocker, subtests):
|
||||
def test_estimator_basics(self, subtests):
|
||||
for lag_frames in range(5):
|
||||
with subtests.test(msg=f"lag_frames={lag_frames}"):
|
||||
mocked_CP = mocker.Mock(steerActuatorDelay=0.8)
|
||||
mocked_CP = car.CarParams(steerActuatorDelay=0.8)
|
||||
estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0)
|
||||
process_messages(mocker, estimator, lag_frames, int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_NUM_NEEDED * BLOCK_SIZE)
|
||||
process_messages(estimator, lag_frames, int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_NUM_NEEDED * BLOCK_SIZE)
|
||||
msg = estimator.get_msg(True)
|
||||
assert msg.liveDelay.status == 'estimated'
|
||||
assert np.allclose(msg.liveDelay.lateralDelay, lag_frames * DT, atol=0.01)
|
||||
assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
|
||||
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
|
||||
assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED
|
||||
assert msg.liveDelay.calPerc == 100
|
||||
|
||||
def test_estimator_masking(self, mocker):
|
||||
mocked_CP, lag_frames = mocker.Mock(steerActuatorDelay=0.8), random.randint(1, 19)
|
||||
def test_disabled_estimator(self):
|
||||
mocked_CP = car.CarParams(steerActuatorDelay=0.8)
|
||||
estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0, enabled=False)
|
||||
lag_frames = 5
|
||||
process_messages(estimator, lag_frames, int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_NUM_NEEDED * BLOCK_SIZE)
|
||||
msg = estimator.get_msg(True)
|
||||
assert msg.liveDelay.status == 'unestimated'
|
||||
assert np.allclose(msg.liveDelay.lateralDelay, 1.0, atol=0.01)
|
||||
assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
|
||||
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
|
||||
assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED
|
||||
assert msg.liveDelay.calPerc == 100
|
||||
|
||||
def test_estimator_masking(self):
|
||||
mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.8), random.randint(1, 19)
|
||||
estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0, min_valid_block_count=1)
|
||||
process_messages(mocker, estimator, lag_frames, (int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_SIZE) * 2, rejection_threshold=0.4)
|
||||
process_messages(estimator, lag_frames, (int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_SIZE) * 2, rejection_threshold=0.4)
|
||||
msg = estimator.get_msg(True)
|
||||
assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
|
||||
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
|
||||
assert msg.liveDelay.calPerc == 100
|
||||
|
||||
@pytest.mark.skipif(PC, reason="only on device")
|
||||
@pytest.mark.timeout(60)
|
||||
def test_estimator_performance(self, mocker):
|
||||
mocked_CP = mocker.Mock(steerActuatorDelay=0.8)
|
||||
def test_estimator_performance(self):
|
||||
mocked_CP = car.CarParams(steerActuatorDelay=0.8)
|
||||
estimator = LateralLagEstimator(mocked_CP, DT)
|
||||
|
||||
ds = []
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from cereal import car
|
||||
from openpilot.selfdrive.locationd.torqued import TorqueEstimator
|
||||
|
||||
|
||||
def test_cal_percent():
|
||||
est = TorqueEstimator(car.CarParams())
|
||||
msg = est.get_msg()
|
||||
assert msg.liveTorqueParameters.calPerc == 0
|
||||
|
||||
for (low, high), min_pts in zip(est.filtered_points.buckets.keys(),
|
||||
est.filtered_points.buckets_min_points.values(), strict=True):
|
||||
for _ in range(int(min_pts)):
|
||||
est.filtered_points.add_point((low + high) / 2.0, 0.0)
|
||||
|
||||
# enough bucket points, but not enough total points
|
||||
msg = est.get_msg()
|
||||
assert msg.liveTorqueParameters.calPerc == (len(est.filtered_points) / est.min_points_total * 100 + 100) / 2
|
||||
|
||||
# add enough points to bucket with most capacity
|
||||
key = list(est.filtered_points.buckets)[0]
|
||||
for _ in range(est.min_points_total - len(est.filtered_points)):
|
||||
est.filtered_points.add_point((key[0] + key[1]) / 2.0, 0.0)
|
||||
|
||||
msg = est.get_msg()
|
||||
assert msg.liveTorqueParameters.calPerc == 100
|
||||
@@ -233,6 +233,7 @@ class TorqueEstimator(ParameterEstimator):
|
||||
liveTorqueParameters.latAccelOffsetFiltered = float(self.filtered_params['latAccelOffset'].x)
|
||||
liveTorqueParameters.frictionCoefficientFiltered = float(self.filtered_params['frictionCoefficient'].x)
|
||||
liveTorqueParameters.totalBucketPoints = len(self.filtered_points)
|
||||
liveTorqueParameters.calPerc = self.filtered_points.get_valid_percent()
|
||||
liveTorqueParameters.decay = self.decay
|
||||
liveTorqueParameters.maxResets = self.resets
|
||||
return msg
|
||||
|
||||
@@ -60,7 +60,7 @@ import subprocess
|
||||
from tinygrad import Device
|
||||
|
||||
# because tg doesn't support multi-process
|
||||
devs = subprocess.check_output('python3 -c "from tinygrad import Device; print(list(Device.get_available_devices()))"', shell=True)
|
||||
devs = subprocess.check_output('python3 -c "from tinygrad import Device; print(list(Device.get_available_devices()))"', shell=True, cwd=env.Dir('#').abspath)
|
||||
if b"AMD" in devs:
|
||||
del Device
|
||||
print("USB GPU detected... building")
|
||||
|
||||
+20
-21
@@ -86,10 +86,20 @@ class ModelState:
|
||||
prev_desire: np.ndarray # for tracking the rising edge of the pulse
|
||||
|
||||
def __init__(self, context: CLContext):
|
||||
self.frames = {
|
||||
'input_imgs': DrivingModelFrame(context, ModelConstants.TEMPORAL_SKIP),
|
||||
'big_input_imgs': DrivingModelFrame(context, ModelConstants.TEMPORAL_SKIP)
|
||||
}
|
||||
with open(VISION_METADATA_PATH, 'rb') as f:
|
||||
vision_metadata = pickle.load(f)
|
||||
self.vision_input_shapes = vision_metadata['input_shapes']
|
||||
self.vision_input_names = list(self.vision_input_shapes.keys())
|
||||
self.vision_output_slices = vision_metadata['output_slices']
|
||||
vision_output_size = vision_metadata['output_shapes']['outputs'][1]
|
||||
|
||||
with open(POLICY_METADATA_PATH, 'rb') as f:
|
||||
policy_metadata = pickle.load(f)
|
||||
self.policy_input_shapes = policy_metadata['input_shapes']
|
||||
self.policy_output_slices = policy_metadata['output_slices']
|
||||
policy_output_size = policy_metadata['output_shapes']['outputs'][1]
|
||||
|
||||
self.frames = {name: DrivingModelFrame(context, ModelConstants.TEMPORAL_SKIP) for name in self.vision_input_names}
|
||||
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
|
||||
|
||||
self.full_features_buffer = np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32)
|
||||
@@ -106,18 +116,6 @@ class ModelState:
|
||||
'features_buffer': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32),
|
||||
}
|
||||
|
||||
with open(VISION_METADATA_PATH, 'rb') as f:
|
||||
vision_metadata = pickle.load(f)
|
||||
self.vision_input_shapes = vision_metadata['input_shapes']
|
||||
self.vision_output_slices = vision_metadata['output_slices']
|
||||
vision_output_size = vision_metadata['output_shapes']['outputs'][1]
|
||||
|
||||
with open(POLICY_METADATA_PATH, 'rb') as f:
|
||||
policy_metadata = pickle.load(f)
|
||||
self.policy_input_shapes = policy_metadata['input_shapes']
|
||||
self.policy_output_slices = policy_metadata['output_slices']
|
||||
policy_output_size = policy_metadata['output_shapes']['outputs'][1]
|
||||
|
||||
# img buffers are managed in openCL transform code
|
||||
self.vision_inputs: dict[str, Tensor] = {}
|
||||
self.vision_output = np.zeros(vision_output_size, dtype=np.float32)
|
||||
@@ -135,7 +133,7 @@ class ModelState:
|
||||
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
|
||||
return parsed_model_outputs
|
||||
|
||||
def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray,
|
||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
||||
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
|
||||
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
||||
inputs['desire'][0] = 0
|
||||
@@ -148,8 +146,7 @@ class ModelState:
|
||||
|
||||
self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention']
|
||||
self.numpy_inputs['lateral_control_params'][:] = inputs['lateral_control_params']
|
||||
imgs_cl = {'input_imgs': self.frames['input_imgs'].prepare(buf, transform.flatten()),
|
||||
'big_input_imgs': self.frames['big_input_imgs'].prepare(wbuf, transform_wide.flatten())}
|
||||
imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.vision_input_names}
|
||||
|
||||
if TICI and not USBGPU:
|
||||
# The imgs tensors are backed by opencl memory, only need init once
|
||||
@@ -328,14 +325,16 @@ def main(demo=False):
|
||||
if prepare_only:
|
||||
cloudlog.error(f"skipping model eval. Dropped {vipc_dropped_frames} frames")
|
||||
|
||||
bufs = {name: buf_extra if 'big' in name else buf_main for name in model.vision_input_names}
|
||||
transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.vision_input_names}
|
||||
inputs:dict[str, np.ndarray] = {
|
||||
'desire': vec_desire,
|
||||
'traffic_convention': traffic_convention,
|
||||
'lateral_control_params': lateral_control_params,
|
||||
}
|
||||
}
|
||||
|
||||
mt1 = time.perf_counter()
|
||||
model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only)
|
||||
model_output = model.run(bufs, transforms, inputs, prepare_only)
|
||||
mt2 = time.perf_counter()
|
||||
model_execution_time = mt2 - mt1
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:98f0121ccb6f850077b04cc91bd33d370fc6cbdc2bd35f1ab55628a15a813f36
|
||||
size 15966721
|
||||
oid sha256:5f714fb38bcd44b5d9f44e3a8e1595e1dfdf7558f0eec3485cf3f2dbb6dc7d8d
|
||||
size 15971805
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:897f80d0388250f99bba69b6a8434560cc0fd83157cbeb0bc134c67fe4e64624
|
||||
size 34882971
|
||||
oid sha256:3ac4867fbc618037e8d03143edbfeeae960f2025644b5dcf36c6665271b4f874
|
||||
size 34883375
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
"text": "sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield.",
|
||||
"severity": 0
|
||||
},
|
||||
"OffroadMode_Status": {
|
||||
"text": "sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to \"Settings\" -> \"Device\" to exit Always Offroad mode.",
|
||||
"severity": 1
|
||||
"Offroad_OSMUpdateRequired": {
|
||||
"text": "OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display.\n\n%1",
|
||||
"severity": 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,11 +585,6 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
},
|
||||
|
||||
EventName.noGps: {
|
||||
ET.PERMANENT: Alert(
|
||||
"Poor GPS reception",
|
||||
"Ensure device has a clear view of the sky",
|
||||
AlertStatus.normal, AlertSize.mid,
|
||||
Priority.LOWER, VisualAlert.none, AudibleAlert.none, .2, creation_delay=600.)
|
||||
},
|
||||
|
||||
EventName.tooDistracted: {
|
||||
|
||||
@@ -129,8 +129,10 @@ class SelfdriveD(CruiseHelper):
|
||||
# some comma three with NVMe experience NVMe dropouts mid-drive that
|
||||
# cause loggerd to crash on write, so ignore it only on that platform
|
||||
self.ignored_processes = set()
|
||||
if HARDWARE.get_device_type() == 'tici' and os.path.exists('/dev/nvme0'):
|
||||
nvme_expected = os.path.exists('/dev/nvme0n1') or (not os.path.isfile("/persist/comma/living-in-the-moment"))
|
||||
if HARDWARE.get_device_type() == 'tici' and nvme_expected:
|
||||
self.ignored_processes = {'loggerd', }
|
||||
self.ignored_processes.update({'mapd'})
|
||||
|
||||
# Determine startup event
|
||||
self.startup_event = EventName.startup if build_metadata.openpilot.comma_remote and build_metadata.tested_channel else EventName.startupMaster
|
||||
@@ -380,16 +382,16 @@ class SelfdriveD(CruiseHelper):
|
||||
if (planner_fcw or model_fcw) and not self.CP.notCar:
|
||||
self.events.add(EventName.fcw)
|
||||
|
||||
# GPS checks
|
||||
gps_ok = self.sm.recv_frame[self.gps_location_service] > 0 and (self.sm.frame - self.sm.recv_frame[self.gps_location_service]) * DT_CTRL < 2.0
|
||||
if not gps_ok and self.sm['livePose'].inputsOK and (self.distance_traveled > 1500):
|
||||
self.events.add(EventName.noGps)
|
||||
if gps_ok:
|
||||
self.distance_traveled = 0
|
||||
self.distance_traveled += abs(CS.vEgo) * DT_CTRL
|
||||
|
||||
# TODO: fix simulator
|
||||
if not SIMULATION or REPLAY:
|
||||
# Not show in first 1.5 km to allow for driving out of garage. This event shows after 5 minutes
|
||||
gps_ok = self.sm.recv_frame[self.gps_location_service] > 0 and (self.sm.frame - self.sm.recv_frame[self.gps_location_service]) * DT_CTRL < 2.0
|
||||
if not gps_ok and self.sm['livePose'].inputsOK and (self.distance_traveled > 1500):
|
||||
self.events.add(EventName.noGps)
|
||||
if gps_ok:
|
||||
self.distance_traveled = 0
|
||||
self.distance_traveled += abs(CS.vEgo) * DT_CTRL
|
||||
|
||||
if self.sm['modelV2'].frameDropPerc > 20:
|
||||
self.events.add(EventName.modeldLagging)
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
|
||||
OP_ROOT="$DIR/../../"
|
||||
|
||||
if [ -z "$BUILD" ]; then
|
||||
docker pull ghcr.io/commaai/openpilot-base:latest
|
||||
else
|
||||
docker build --cache-from ghcr.io/commaai/openpilot-base:latest -t ghcr.io/commaai/openpilot-base:latest -f $OP_ROOT/Dockerfile.openpilot_base .
|
||||
fi
|
||||
|
||||
docker run \
|
||||
-it \
|
||||
--rm \
|
||||
--volume $OP_ROOT:$OP_ROOT \
|
||||
--workdir $PWD \
|
||||
--env PYTHONPATH=$OP_ROOT \
|
||||
ghcr.io/commaai/openpilot-base:latest \
|
||||
/bin/bash
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from typing import Any
|
||||
@@ -15,15 +16,15 @@ from openpilot.system.hardware import PC
|
||||
from openpilot.tools.lib.openpilotci import get_url
|
||||
from openpilot.selfdrive.test.process_replay.compare_logs import compare_logs, format_diff
|
||||
from openpilot.selfdrive.test.process_replay.process_replay import get_process_config, replay_process
|
||||
from openpilot.tools.lib.framereader import FrameReader, NumpyFrameReader
|
||||
from openpilot.tools.lib.framereader import FrameReader
|
||||
from openpilot.tools.lib.logreader import LogReader, save_log
|
||||
from openpilot.tools.lib.github_utils import GithubUtils
|
||||
|
||||
TEST_ROUTE = "8494c69d3c710e81|000001d4--2648a9a404"
|
||||
SEGMENT = 4
|
||||
MAX_FRAMES = 100 if PC else 400
|
||||
START_FRAME = 0
|
||||
END_FRAME = 60
|
||||
|
||||
NO_MODEL = "NO_MODEL" in os.environ
|
||||
SEND_EXTRA_INPUTS = bool(int(os.getenv("SEND_EXTRA_INPUTS", "0")))
|
||||
|
||||
DATA_TOKEN = os.getenv("CI_ARTIFACTS_TOKEN","")
|
||||
@@ -125,16 +126,15 @@ def comment_replay_report(proposed, master, full_logs):
|
||||
comment = f"ref for commit {commit}: {link}/{log_name}" + diff_plots + all_plots
|
||||
GITHUB.comment_on_pr(comment, PR_BRANCH, "commaci-public", True)
|
||||
|
||||
def trim_logs_to_max_frames(logs, max_frames, frs_types, include_all_types):
|
||||
def trim_logs(logs, start_frame, end_frame, frs_types, include_all_types):
|
||||
all_msgs = []
|
||||
cam_state_counts = defaultdict(int)
|
||||
# keep adding messages until cam states are equal to MAX_FRAMES
|
||||
for msg in sorted(logs, key=lambda m: m.logMonoTime):
|
||||
all_msgs.append(msg)
|
||||
if msg.which() in frs_types:
|
||||
cam_state_counts[msg.which()] += 1
|
||||
|
||||
if all(cam_state_counts[state] == max_frames for state in frs_types):
|
||||
if any(cam_state_counts[state] >= start_frame for state in frs_types):
|
||||
all_msgs.append(msg)
|
||||
if all(cam_state_counts[state] == end_frame for state in frs_types):
|
||||
break
|
||||
|
||||
if len(include_all_types) != 0:
|
||||
@@ -146,9 +146,9 @@ def trim_logs_to_max_frames(logs, max_frames, frs_types, include_all_types):
|
||||
|
||||
def model_replay(lr, frs):
|
||||
# modeld is using frame pairs
|
||||
modeld_logs = trim_logs_to_max_frames(lr, MAX_FRAMES, {"roadCameraState", "wideRoadCameraState"},
|
||||
{"roadEncodeIdx", "wideRoadEncodeIdx", "carParams", "carState", "carControl"})
|
||||
dmodeld_logs = trim_logs_to_max_frames(lr, MAX_FRAMES, {"driverCameraState"}, {"driverEncodeIdx", "carParams"})
|
||||
modeld_logs = trim_logs(lr, START_FRAME, END_FRAME, {"roadCameraState", "wideRoadCameraState"},
|
||||
{"roadEncodeIdx", "wideRoadEncodeIdx", "carParams", "carState", "carControl", "can"})
|
||||
dmodeld_logs = trim_logs(lr, START_FRAME, END_FRAME, {"driverCameraState"}, {"driverEncodeIdx", "carParams", "can"})
|
||||
|
||||
if not SEND_EXTRA_INPUTS:
|
||||
modeld_logs = [msg for msg in modeld_logs if msg.which() != 'liveCalibration']
|
||||
@@ -165,9 +165,6 @@ def model_replay(lr, frs):
|
||||
dmonitoringmodeld = get_process_config("dmonitoringmodeld")
|
||||
|
||||
modeld_msgs = replay_process(modeld, modeld_logs, frs)
|
||||
if isinstance(frs['roadCameraState'], NumpyFrameReader):
|
||||
del frs['roadCameraState'].frames
|
||||
del frs['wideRoadCameraState'].frames
|
||||
dmonitoringmodeld_msgs = replay_process(dmonitoringmodeld, dmodeld_logs, frs)
|
||||
|
||||
msgs = modeld_msgs + dmonitoringmodeld_msgs
|
||||
@@ -193,34 +190,36 @@ def model_replay(lr, frs):
|
||||
print("----------------- Model Timing -----------------")
|
||||
print("------------------------------------------------")
|
||||
print(tabulate(rows, header, tablefmt="simple_grid", stralign="center", numalign="center", floatfmt=".4f"))
|
||||
assert timings_ok
|
||||
assert timings_ok or PC
|
||||
|
||||
return msgs
|
||||
|
||||
|
||||
def get_frames():
|
||||
regen_cache = "--regen-cache" in sys.argv
|
||||
cache = "--cache" in sys.argv or not PC or regen_cache
|
||||
videos = ('fcamera.hevc', 'dcamera.hevc', 'ecamera.hevc')
|
||||
cams = ('roadCameraState', 'driverCameraState', 'wideRoadCameraState')
|
||||
frames_cache = '/tmp/model_replay_cache' if PC else '/data/model_replay_cache'
|
||||
os.makedirs(frames_cache, exist_ok=True)
|
||||
|
||||
if cache:
|
||||
frames_cache = '/tmp/model_replay_cache' if PC else '/data/model_replay_cache'
|
||||
os.makedirs(frames_cache, exist_ok=True)
|
||||
|
||||
cache_size = 200
|
||||
for v in videos:
|
||||
if not all(os.path.isfile(f'{frames_cache}/{TEST_ROUTE}_{v}_{i}.npy') for i in range(MAX_FRAMES//cache_size)) or regen_cache:
|
||||
f = FrameReader(get_url(TEST_ROUTE, SEGMENT, v)).get(0, MAX_FRAMES + 1, pix_fmt="nv12")
|
||||
print(f'Caching {v}...')
|
||||
for i in range(MAX_FRAMES//cache_size):
|
||||
np.save(f'{frames_cache}/{TEST_ROUTE}_{v}_{i}', f[(i * cache_size) + 1:((i + 1) * cache_size) + 1])
|
||||
del f
|
||||
|
||||
return {c : NumpyFrameReader(f"{frames_cache}/{TEST_ROUTE}_{v}", 1928, 1208, cache_size) for c,v in zip(cams, videos, strict=True)}
|
||||
else:
|
||||
return {c : FrameReader(get_url(TEST_ROUTE, SEGMENT, v), readahead=True) for c,v in zip(cams, videos, strict=True)}
|
||||
cache_name = f'{frames_cache}/{TEST_ROUTE}_{SEGMENT}_{START_FRAME}_{END_FRAME}.pkl'
|
||||
if os.path.isfile(cache_name) and not regen_cache:
|
||||
try:
|
||||
print(f"Loading frames from cache {cache_name}")
|
||||
return pickle.load(open(cache_name, "rb"))
|
||||
except Exception as e:
|
||||
print(f"Failed to load frames from cache {cache_name}: {e}")
|
||||
|
||||
frs = {
|
||||
'roadCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "fcamera.hevc"), pix_fmt='nv12', cache_size=END_FRAME - START_FRAME),
|
||||
'driverCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "dcamera.hevc"), pix_fmt='nv12', cache_size=END_FRAME - START_FRAME),
|
||||
'wideRoadCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "ecamera.hevc"), pix_fmt='nv12', cache_size=END_FRAME - START_FRAME),
|
||||
}
|
||||
for fr in frs.values():
|
||||
for fidx in range(START_FRAME, END_FRAME):
|
||||
fr.get(fidx)
|
||||
fr.it = None
|
||||
print(f"Dumping frame cache {cache_name}")
|
||||
pickle.dump(frs, open(cache_name, "wb"))
|
||||
return frs
|
||||
|
||||
if __name__ == "__main__":
|
||||
update = "--update" in sys.argv or (os.getenv("GIT_BRANCH", "") == 'master')
|
||||
@@ -232,8 +231,7 @@ if __name__ == "__main__":
|
||||
|
||||
log_msgs = []
|
||||
# run replays
|
||||
if not NO_MODEL:
|
||||
log_msgs += model_replay(lr, frs)
|
||||
log_msgs += model_replay(lr, frs)
|
||||
|
||||
# get diff
|
||||
failed = False
|
||||
@@ -242,13 +240,10 @@ if __name__ == "__main__":
|
||||
try:
|
||||
all_logs = list(LogReader(GITHUB.get_file_url(MODEL_REPLAY_BUCKET, log_fn)))
|
||||
cmp_log = []
|
||||
|
||||
# logs are ordered based on type: modelV2, drivingModelData, driverStateV2
|
||||
if not NO_MODEL:
|
||||
model_start_index = next(i for i, m in enumerate(all_logs) if m.which() in ("modelV2", "drivingModelData", "cameraOdometry"))
|
||||
cmp_log += all_logs[model_start_index:model_start_index + MAX_FRAMES*3]
|
||||
dmon_start_index = next(i for i, m in enumerate(all_logs) if m.which() == "driverStateV2")
|
||||
cmp_log += all_logs[dmon_start_index:dmon_start_index + MAX_FRAMES]
|
||||
model_start_index = next(i for i, m in enumerate(all_logs) if m.which() in ("modelV2", "drivingModelData", "cameraOdometry"))
|
||||
cmp_log += all_logs[model_start_index+START_FRAME*3:model_start_index + END_FRAME*3]
|
||||
dmon_start_index = next(i for i, m in enumerate(all_logs) if m.which() == "driverStateV2")
|
||||
cmp_log += all_logs[dmon_start_index+START_FRAME:dmon_start_index + END_FRAME]
|
||||
|
||||
ignore = [
|
||||
'logMonoTime',
|
||||
|
||||
@@ -27,7 +27,7 @@ from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera
|
||||
from openpilot.selfdrive.test.process_replay.migration import migrate_all
|
||||
from openpilot.selfdrive.test.process_replay.capture import ProcessOutputCapture
|
||||
from openpilot.tools.lib.logreader import LogIterable
|
||||
from openpilot.tools.lib.framereader import BaseFrameReader
|
||||
from openpilot.tools.lib.framereader import FrameReader
|
||||
|
||||
# Numpy gives different results based on CPU features after version 19
|
||||
NUMPY_TOLERANCE = 1e-7
|
||||
@@ -209,6 +209,7 @@ class ProcessContainer:
|
||||
streams_metas = available_streams(all_msgs)
|
||||
for meta in streams_metas:
|
||||
if meta.camera_state in self.cfg.vision_pubs:
|
||||
assert frs[meta.camera_state].pix_fmt == 'nv12'
|
||||
frame_size = (frs[meta.camera_state].w, frs[meta.camera_state].h)
|
||||
vipc_server.create_buffers(meta.stream, 2, *frame_size)
|
||||
vipc_server.start_listener()
|
||||
@@ -224,7 +225,7 @@ class ProcessContainer:
|
||||
|
||||
def start(
|
||||
self, params_config: dict[str, Any], environ_config: dict[str, Any],
|
||||
all_msgs: LogIterable, frs: dict[str, BaseFrameReader] | None,
|
||||
all_msgs: LogIterable, frs: dict[str, FrameReader] | None,
|
||||
fingerprint: str | None, capture_output: bool
|
||||
):
|
||||
with self.prefix as p:
|
||||
@@ -266,7 +267,7 @@ class ProcessContainer:
|
||||
self.prefix.clean_dirs()
|
||||
self._clean_env()
|
||||
|
||||
def run_step(self, msg: capnp._DynamicStructReader, frs: dict[str, BaseFrameReader] | None) -> list[capnp._DynamicStructReader]:
|
||||
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 = []
|
||||
@@ -296,7 +297,7 @@ class ProcessContainer:
|
||||
camera_state = getattr(m, m.which())
|
||||
camera_meta = meta_from_camera_state(m.which())
|
||||
assert frs is not None
|
||||
img = frs[m.which()].get(camera_state.frameId, pix_fmt="nv12")[0]
|
||||
img = frs[m.which()].get(camera_state.frameId)
|
||||
self.vipc_server.send(camera_meta.stream, img.flatten().tobytes(),
|
||||
camera_state.frameId, camera_state.timestampSof, camera_state.timestampEof)
|
||||
self.msg_queue = []
|
||||
@@ -655,7 +656,7 @@ def replay_process_with_name(name: str | Iterable[str], lr: LogIterable, *args,
|
||||
|
||||
|
||||
def replay_process(
|
||||
cfg: ProcessConfig | Iterable[ProcessConfig], lr: LogIterable, frs: dict[str, BaseFrameReader] = None,
|
||||
cfg: ProcessConfig | Iterable[ProcessConfig], lr: LogIterable, frs: dict[str, FrameReader] = None,
|
||||
fingerprint: str = None, return_all_logs: bool = False, custom_params: dict[str, Any] = None,
|
||||
captured_output_store: dict[str, dict[str, str]] = None, disable_progress: bool = False
|
||||
) -> list[capnp._DynamicStructReader]:
|
||||
@@ -683,7 +684,7 @@ def replay_process(
|
||||
|
||||
|
||||
def _replay_multi_process(
|
||||
cfgs: list[ProcessConfig], lr: LogIterable, frs: dict[str, BaseFrameReader] | None, fingerprint: str | None,
|
||||
cfgs: list[ProcessConfig], lr: LogIterable, frs: dict[str, FrameReader] | None, fingerprint: str | None,
|
||||
custom_params: dict[str, Any] | None, captured_output_store: dict[str, dict[str, str]] | None, disable_progress: bool
|
||||
) -> list[capnp._DynamicStructReader]:
|
||||
if fingerprint is not None:
|
||||
|
||||
@@ -1 +1 @@
|
||||
9e2fe2942fbf77f24bccdbef15893831f9c0b390
|
||||
f440c9e0469d32d350aa99ddaa8f44591a2ce690
|
||||
@@ -3,40 +3,17 @@ import os
|
||||
import argparse
|
||||
import time
|
||||
import capnp
|
||||
import numpy as np
|
||||
|
||||
from typing import Any
|
||||
from collections.abc import Iterable
|
||||
|
||||
from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, FAKEDATA, ProcessConfig, replay_process, get_process_config, \
|
||||
check_openpilot_enabled, check_most_messages_valid, get_custom_params_from_lr
|
||||
from openpilot.selfdrive.test.process_replay.vision_meta import DRIVER_CAMERA_FRAME_SIZES
|
||||
from openpilot.selfdrive.test.update_ci_routes import upload_route
|
||||
from openpilot.tools.lib.framereader import FrameReader, BaseFrameReader, FrameType
|
||||
from openpilot.tools.lib.framereader import FrameReader
|
||||
from openpilot.tools.lib.logreader import LogReader, LogIterable, save_log
|
||||
from openpilot.tools.lib.openpilotci import get_url
|
||||
|
||||
class DummyFrameReader(BaseFrameReader):
|
||||
def __init__(self, w: int, h: int, frame_count: int, pix_val: int):
|
||||
self.pix_val = pix_val
|
||||
self.w, self.h = w, h
|
||||
self.frame_count = frame_count
|
||||
self.frame_type = FrameType.raw
|
||||
|
||||
def get(self, idx, count=1, pix_fmt="yuv420p"):
|
||||
if pix_fmt == "rgb24":
|
||||
shape = (self.h, self.w, 3)
|
||||
elif pix_fmt == "nv12" or pix_fmt == "yuv420p":
|
||||
shape = (int((self.h * self.w) * 3 / 2),)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
return [np.full(shape, self.pix_val, dtype=np.uint8) for _ in range(count)]
|
||||
|
||||
@staticmethod
|
||||
def zero_dcamera():
|
||||
return DummyFrameReader(*DRIVER_CAMERA_FRAME_SIZES[("tici", "ar0231")], 1200, 0)
|
||||
|
||||
|
||||
def regen_segment(
|
||||
lr: LogIterable, frs: dict[str, Any] = None,
|
||||
@@ -64,7 +41,7 @@ def setup_data_readers(
|
||||
frs['wideRoadCameraState'] = FrameReader(get_url(route, str(sidx), "ecamera.hevc"))
|
||||
if needs_driver_cam:
|
||||
if dummy_driver_cam:
|
||||
frs['driverCameraState'] = DummyFrameReader.zero_dcamera()
|
||||
frs['driverCameraState'] = FrameReader(get_url(route, str(sidx), "fcamera.hevc")) # Use fcam as dummy
|
||||
else:
|
||||
device_type = next(str(msg.initData.deviceType) for msg in lr if msg.which() == "initData")
|
||||
assert device_type != "neo", "Driver camera not supported on neo segments. Use dummy dcamera."
|
||||
|
||||
@@ -19,7 +19,6 @@ from openpilot.tools.lib.logreader import LogReader, save_log
|
||||
IS_AZURE_TOKEN_DEFINED = os.getenv("AZURE_TOKEN")
|
||||
|
||||
source_segments = [
|
||||
("BODY", "937ccb7243511b65|2022-05-24--16-03-09--1"), # COMMA.COMMA_BODY
|
||||
("HYUNDAI", "02c45f73a2e5c6e9|2021-01-01--19-08-22--1"), # HYUNDAI.HYUNDAI_SONATA
|
||||
("HYUNDAI2", "d545129f3ca90f28|2022-11-07--20-43-08--3"), # HYUNDAI.HYUNDAI_KIA_EV6 (+ QCOM GPS)
|
||||
("TOYOTA", "0982d79ebb0de295|2021-01-04--17-13-21--13"), # TOYOTA.TOYOTA_PRIUS
|
||||
@@ -44,7 +43,6 @@ source_segments = [
|
||||
]
|
||||
|
||||
segments = [
|
||||
("BODY", "regen2F3C7259F1B|2025-04-08--23-00-23--0"),
|
||||
("HYUNDAI", "regenAA0FC4ED71E|2025-04-08--22-57-50--0"),
|
||||
("HYUNDAI2", "regenAFB9780D823|2025-04-08--23-00-34--0"),
|
||||
("TOYOTA", "regen218A4DCFAA1|2025-04-08--22-57-51--0"),
|
||||
@@ -65,7 +63,7 @@ segments = [
|
||||
]
|
||||
|
||||
# dashcamOnly makes don't need to be tested until a full port is done
|
||||
excluded_interfaces = ["mock", "tesla"]
|
||||
excluded_interfaces = ["mock", "body"]
|
||||
|
||||
BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/"
|
||||
REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit")
|
||||
@@ -253,7 +251,7 @@ if __name__ == "__main__":
|
||||
continue
|
||||
|
||||
# to speed things up, we only test all segments on card
|
||||
if cfg.proc_name != 'card' and car_brand not in ('HYUNDAI', 'TOYOTA', 'HONDA', 'SUBARU', 'FORD', 'RIVIAN', 'TESLA'):
|
||||
if cfg.proc_name not in ('card', 'controlsd', 'lagd') and car_brand not in ('HYUNDAI', 'TOYOTA'):
|
||||
continue
|
||||
|
||||
cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from parameterized import parameterized
|
||||
|
||||
from openpilot.selfdrive.test.process_replay.regen import regen_segment, DummyFrameReader
|
||||
from openpilot.selfdrive.test.process_replay.regen import regen_segment
|
||||
from openpilot.selfdrive.test.process_replay.process_replay import check_openpilot_enabled
|
||||
from openpilot.tools.lib.openpilotci import get_url
|
||||
from openpilot.tools.lib.logreader import LogReader
|
||||
@@ -18,7 +18,7 @@ def ci_setup_data_readers(route, sidx):
|
||||
lr = LogReader(get_url(route, sidx, "rlog.bz2"))
|
||||
frs = {
|
||||
'roadCameraState': FrameReader(get_url(route, sidx, "fcamera.hevc")),
|
||||
'driverCameraState': DummyFrameReader.zero_dcamera()
|
||||
'driverCameraState': FrameReader(get_url(route, sidx, "fcamera.hevc")),
|
||||
}
|
||||
if next((True for m in lr if m.which() == "wideRoadCameraState"), False):
|
||||
frs["wideRoadCameraState"] = FrameReader(get_url(route, sidx, "ecamera.hevc"))
|
||||
|
||||
@@ -112,7 +112,7 @@ if GetOption('extras'):
|
||||
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], LIBS=raylib_libs)
|
||||
# keep installers small
|
||||
assert f[0].get_size() < 1300*1e3, f[0].get_size()
|
||||
assert f[0].get_size() < 1900*1e3, f[0].get_size()
|
||||
|
||||
# build watch3
|
||||
if arch in ['x86_64', 'aarch64', 'Darwin'] or GetOption('extras'):
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import time
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from enum import IntEnum
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.widgets.offroad_alerts import UpdateAlert, OffroadAlert
|
||||
from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButton
|
||||
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
|
||||
from openpilot.selfdrive.ui.widgets.setup import SetupWidget
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
|
||||
HEADER_HEIGHT = 80
|
||||
HEAD_BUTTON_FONT_SIZE = 40
|
||||
CONTENT_MARGIN = 40
|
||||
SPACING = 25
|
||||
RIGHT_COLUMN_WIDTH = 750
|
||||
REFRESH_INTERVAL = 10.0
|
||||
|
||||
PRIME_BG_COLOR = rl.Color(51, 51, 51, 255)
|
||||
|
||||
|
||||
class HomeLayoutState(IntEnum):
|
||||
HOME = 0
|
||||
UPDATE = 1
|
||||
ALERTS = 2
|
||||
|
||||
|
||||
class HomeLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.params = Params()
|
||||
|
||||
self.update_alert = UpdateAlert()
|
||||
self.offroad_alert = OffroadAlert()
|
||||
|
||||
self.current_state = HomeLayoutState.HOME
|
||||
self.last_refresh = 0
|
||||
self.settings_callback: callable | None = None
|
||||
|
||||
self.update_available = False
|
||||
self.alert_count = 0
|
||||
|
||||
self.header_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self.content_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self.left_column_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self.right_column_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
self.update_notif_rect = rl.Rectangle(0, 0, 200, HEADER_HEIGHT - 10)
|
||||
self.alert_notif_rect = rl.Rectangle(0, 0, 220, HEADER_HEIGHT - 10)
|
||||
|
||||
self._prime_widget = PrimeWidget()
|
||||
self._setup_widget = SetupWidget()
|
||||
|
||||
self._exp_mode_button = ExperimentalModeButton()
|
||||
self._setup_callbacks()
|
||||
|
||||
def _setup_callbacks(self):
|
||||
self.update_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
|
||||
self.offroad_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
|
||||
|
||||
def set_settings_callback(self, callback: Callable):
|
||||
self.settings_callback = callback
|
||||
|
||||
def _set_state(self, state: HomeLayoutState):
|
||||
self.current_state = state
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
current_time = time.time()
|
||||
if current_time - self.last_refresh >= REFRESH_INTERVAL:
|
||||
self._refresh()
|
||||
self.last_refresh = current_time
|
||||
|
||||
self._handle_input()
|
||||
self._render_header()
|
||||
|
||||
# Render content based on current state
|
||||
if self.current_state == HomeLayoutState.HOME:
|
||||
self._render_home_content()
|
||||
elif self.current_state == HomeLayoutState.UPDATE:
|
||||
self._render_update_view()
|
||||
elif self.current_state == HomeLayoutState.ALERTS:
|
||||
self._render_alerts_view()
|
||||
|
||||
def _update_layout_rects(self):
|
||||
self.header_rect = rl.Rectangle(
|
||||
self._rect.x + CONTENT_MARGIN, self._rect.y + CONTENT_MARGIN, self._rect.width - 2 * CONTENT_MARGIN, HEADER_HEIGHT
|
||||
)
|
||||
|
||||
content_y = self._rect.y + CONTENT_MARGIN + HEADER_HEIGHT + SPACING
|
||||
content_height = self._rect.height - CONTENT_MARGIN - HEADER_HEIGHT - SPACING - CONTENT_MARGIN
|
||||
|
||||
self.content_rect = rl.Rectangle(
|
||||
self._rect.x + CONTENT_MARGIN, content_y, self._rect.width - 2 * CONTENT_MARGIN, content_height
|
||||
)
|
||||
|
||||
left_width = self.content_rect.width - RIGHT_COLUMN_WIDTH - SPACING
|
||||
|
||||
self.left_column_rect = rl.Rectangle(self.content_rect.x, self.content_rect.y, left_width, self.content_rect.height)
|
||||
|
||||
self.right_column_rect = rl.Rectangle(
|
||||
self.content_rect.x + left_width + SPACING, self.content_rect.y, RIGHT_COLUMN_WIDTH, self.content_rect.height
|
||||
)
|
||||
|
||||
self.update_notif_rect.x = self.header_rect.x
|
||||
self.update_notif_rect.y = self.header_rect.y + (self.header_rect.height - 60) // 2
|
||||
|
||||
notif_x = self.header_rect.x + (220 if self.update_available else 0)
|
||||
self.alert_notif_rect.x = notif_x
|
||||
self.alert_notif_rect.y = self.header_rect.y + (self.header_rect.height - 60) // 2
|
||||
|
||||
def _handle_input(self):
|
||||
if not rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||
return
|
||||
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
|
||||
if self.update_available and rl.check_collision_point_rec(mouse_pos, self.update_notif_rect):
|
||||
self._set_state(HomeLayoutState.UPDATE)
|
||||
return
|
||||
|
||||
if self.alert_count > 0 and rl.check_collision_point_rec(mouse_pos, self.alert_notif_rect):
|
||||
self._set_state(HomeLayoutState.ALERTS)
|
||||
return
|
||||
|
||||
# Content area input handling
|
||||
if self.current_state == HomeLayoutState.UPDATE:
|
||||
self.update_alert.handle_input(mouse_pos, True)
|
||||
elif self.current_state == HomeLayoutState.ALERTS:
|
||||
self.offroad_alert.handle_input(mouse_pos, True)
|
||||
|
||||
def _render_header(self):
|
||||
font = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
# Update notification button
|
||||
if self.update_available:
|
||||
# Highlight if currently viewing updates
|
||||
highlight_color = rl.Color(255, 140, 40, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(255, 102, 0, 255)
|
||||
rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color)
|
||||
|
||||
text = "UPDATE"
|
||||
text_width = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE).x
|
||||
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_width) // 2
|
||||
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2
|
||||
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
|
||||
|
||||
# Alert notification button
|
||||
if self.alert_count > 0:
|
||||
# Highlight if currently viewing alerts
|
||||
highlight_color = rl.Color(255, 70, 70, 255) if self.current_state == HomeLayoutState.ALERTS else rl.Color(226, 44, 44, 255)
|
||||
rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color)
|
||||
|
||||
alert_text = f"{self.alert_count} ALERT{'S' if self.alert_count > 1 else ''}"
|
||||
text_width = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE).x
|
||||
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_width) // 2
|
||||
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2
|
||||
rl.draw_text_ex(font, alert_text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
|
||||
|
||||
# Version text (right aligned)
|
||||
version_text = self._get_version_text()
|
||||
text_width = measure_text_cached(gui_app.font(FontWeight.NORMAL), version_text, 48).x
|
||||
version_x = self.header_rect.x + self.header_rect.width - text_width
|
||||
version_y = self.header_rect.y + (self.header_rect.height - 48) // 2
|
||||
rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), version_text, rl.Vector2(int(version_x), int(version_y)), 48, 0, DEFAULT_TEXT_COLOR)
|
||||
|
||||
def _render_home_content(self):
|
||||
self._render_left_column()
|
||||
self._render_right_column()
|
||||
|
||||
def _render_update_view(self):
|
||||
self.update_alert.render(self.content_rect)
|
||||
|
||||
def _render_alerts_view(self):
|
||||
self.offroad_alert.render(self.content_rect)
|
||||
|
||||
def _render_left_column(self):
|
||||
self._prime_widget.render(self.left_column_rect)
|
||||
|
||||
def _render_right_column(self):
|
||||
exp_height = 125
|
||||
exp_rect = rl.Rectangle(
|
||||
self.right_column_rect.x, self.right_column_rect.y, self.right_column_rect.width, exp_height
|
||||
)
|
||||
self._exp_mode_button.render(exp_rect)
|
||||
|
||||
setup_rect = rl.Rectangle(
|
||||
self.right_column_rect.x,
|
||||
self.right_column_rect.y + exp_height + SPACING,
|
||||
self.right_column_rect.width,
|
||||
self.right_column_rect.height - exp_height - SPACING,
|
||||
)
|
||||
self._setup_widget.render(setup_rect)
|
||||
|
||||
def _refresh(self):
|
||||
# TODO: implement _update_state with a timer
|
||||
self.update_available = self.update_alert.refresh()
|
||||
self.alert_count = self.offroad_alert.refresh()
|
||||
self._update_state_priority(self.update_available, self.alert_count > 0)
|
||||
|
||||
def _update_state_priority(self, update_available: bool, alerts_present: bool):
|
||||
current_state = self.current_state
|
||||
|
||||
if not update_available and not alerts_present:
|
||||
self.current_state = HomeLayoutState.HOME
|
||||
elif update_available and (current_state == HomeLayoutState.HOME or (not alerts_present and current_state == HomeLayoutState.ALERTS)):
|
||||
self.current_state = HomeLayoutState.UPDATE
|
||||
elif alerts_present and (current_state == HomeLayoutState.HOME or (not update_available and current_state == HomeLayoutState.UPDATE)):
|
||||
self.current_state = HomeLayoutState.ALERTS
|
||||
|
||||
def _get_version_text(self) -> str:
|
||||
brand = "openpilot"
|
||||
description = self.params.get("UpdaterCurrentDescription", encoding='utf-8')
|
||||
return f"{brand} {description}" if description else brand
|
||||
@@ -0,0 +1,93 @@
|
||||
import pyray as rl
|
||||
from enum import IntEnum
|
||||
import cereal.messaging as messaging
|
||||
from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH
|
||||
from openpilot.selfdrive.ui.layouts.home import HomeLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType
|
||||
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
||||
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
|
||||
|
||||
class MainState(IntEnum):
|
||||
HOME = 0
|
||||
SETTINGS = 1
|
||||
ONROAD = 2
|
||||
|
||||
|
||||
class MainLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._pm = messaging.PubMaster(['userFlag'])
|
||||
|
||||
self._sidebar = Sidebar()
|
||||
self._current_mode = MainState.HOME
|
||||
self._prev_onroad = False
|
||||
|
||||
# Initialize layouts
|
||||
self._layouts = {MainState.HOME: HomeLayout(), MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()}
|
||||
|
||||
self._sidebar_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self._content_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
# Set callbacks
|
||||
self._setup_callbacks()
|
||||
|
||||
def _render(self, _):
|
||||
self._handle_onroad_transition()
|
||||
self._render_main_content()
|
||||
|
||||
def _setup_callbacks(self):
|
||||
self._sidebar.set_callbacks(on_settings=self._on_settings_clicked,
|
||||
on_flag=self._on_flag_clicked)
|
||||
self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE))
|
||||
self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state)
|
||||
self._layouts[MainState.ONROAD].set_callbacks(on_click=self._on_onroad_clicked)
|
||||
device.add_interactive_timeout_callback(self._set_mode_for_state)
|
||||
|
||||
def _update_layout_rects(self):
|
||||
self._sidebar_rect = rl.Rectangle(self._rect.x, self._rect.y, SIDEBAR_WIDTH, self._rect.height)
|
||||
|
||||
x_offset = SIDEBAR_WIDTH if self._sidebar.is_visible else 0
|
||||
self._content_rect = rl.Rectangle(self._rect.y + x_offset, self._rect.y, self._rect.width - x_offset, self._rect.height)
|
||||
|
||||
def _handle_onroad_transition(self):
|
||||
if ui_state.started != self._prev_onroad:
|
||||
self._prev_onroad = ui_state.started
|
||||
|
||||
self._set_mode_for_state()
|
||||
|
||||
def _set_mode_for_state(self):
|
||||
if ui_state.started:
|
||||
# Don't hide sidebar from interactive timeout
|
||||
if self._current_mode != MainState.ONROAD:
|
||||
self._sidebar.set_visible(False)
|
||||
self._current_mode = MainState.ONROAD
|
||||
else:
|
||||
self._current_mode = MainState.HOME
|
||||
self._sidebar.set_visible(True)
|
||||
|
||||
def open_settings(self, panel_type: PanelType):
|
||||
self._layouts[MainState.SETTINGS].set_current_panel(panel_type)
|
||||
self._current_mode = MainState.SETTINGS
|
||||
self._sidebar.set_visible(False)
|
||||
|
||||
def _on_settings_clicked(self):
|
||||
self.open_settings(PanelType.DEVICE)
|
||||
|
||||
def _on_flag_clicked(self):
|
||||
user_flag = messaging.new_message('userFlag')
|
||||
user_flag.valid = True
|
||||
self._pm.send('userFlag', user_flag)
|
||||
|
||||
def _on_onroad_clicked(self):
|
||||
self._sidebar.set_visible(not self._sidebar.is_visible)
|
||||
|
||||
def _render_main_content(self):
|
||||
# Render sidebar
|
||||
if self._sidebar.is_visible:
|
||||
self._sidebar.render(self._sidebar_rect)
|
||||
|
||||
content_rect = self._content_rect if self._sidebar.is_visible else self._rect
|
||||
self._layouts[self._current_mode].render(content_rect)
|
||||
@@ -0,0 +1,17 @@
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper
|
||||
from openpilot.system.ui.widgets.network import WifiManagerUI
|
||||
|
||||
|
||||
class NetworkLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.wifi_manager = WifiManagerWrapper()
|
||||
self.wifi_ui = WifiManagerUI(self.wifi_manager)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self.wifi_ui.render(rect)
|
||||
|
||||
def shutdown(self):
|
||||
self.wifi_manager.shutdown()
|
||||
@@ -0,0 +1,61 @@
|
||||
from openpilot.system.ui.lib.list_view import toggle_item
|
||||
from openpilot.system.ui.lib.scroller import Scroller
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
'enable_adb': (
|
||||
"ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. " +
|
||||
"See https://docs.comma.ai/how-to/connect-to-comma for more info."
|
||||
),
|
||||
'joystick_debug_mode': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
|
||||
'ssh_key': (
|
||||
"Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username " +
|
||||
"other than your own. A comma employee will NEVER ask you to add their GitHub username."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class DeveloperLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
items = [
|
||||
toggle_item(
|
||||
"Enable ADB",
|
||||
description=DESCRIPTIONS["enable_adb"],
|
||||
initial_state=self._params.get_bool("AdbEnabled"),
|
||||
callback=self._on_enable_adb,
|
||||
),
|
||||
ssh_key_item("SSH Key", description=DESCRIPTIONS["ssh_key"]),
|
||||
toggle_item(
|
||||
"Joystick Debug Mode",
|
||||
description=DESCRIPTIONS["joystick_debug_mode"],
|
||||
initial_state=self._params.get_bool("JoystickDebugMode"),
|
||||
callback=self._on_joystick_debug_mode,
|
||||
),
|
||||
toggle_item(
|
||||
"Longitudinal Maneuver Mode",
|
||||
description="",
|
||||
initial_state=self._params.get_bool("LongitudinalManeuverMode"),
|
||||
callback=self._on_long_maneuver_mode,
|
||||
),
|
||||
toggle_item(
|
||||
"openpilot Longitudinal Control (Alpha)",
|
||||
description="",
|
||||
initial_state=self._params.get_bool("AlphaLongitudinalEnabled"),
|
||||
callback=self._on_alpha_long_enabled,
|
||||
),
|
||||
]
|
||||
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def _on_enable_adb(self): pass
|
||||
def _on_joystick_debug_mode(self): pass
|
||||
def _on_long_maneuver_mode(self): pass
|
||||
def _on_alpha_long_enabled(self): pass
|
||||
@@ -0,0 +1,150 @@
|
||||
import os
|
||||
import json
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.hardware import TICI
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.list_view import text_item, button_item, dual_button_item
|
||||
from openpilot.system.ui.lib.scroller import Scroller
|
||||
from openpilot.system.ui.lib.widget import Widget, DialogResult
|
||||
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog, alert_dialog
|
||||
from openpilot.system.ui.widgets.html_render import HtmlRenderer
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
'pair_device': "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.",
|
||||
'driver_camera': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
|
||||
'reset_calibration': (
|
||||
"openpilot requires the device to be mounted within 4° left or right and within 5° " +
|
||||
"up or 9° down. openpilot is continuously calibrating, resetting is rarely required."
|
||||
),
|
||||
'review_guide': "Review the rules, features, and limitations of openpilot",
|
||||
}
|
||||
|
||||
|
||||
class DeviceLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
self._select_language_dialog: MultiOptionDialog | None = None
|
||||
self._driver_camera: DriverCameraDialog | None = None
|
||||
self._pair_device_dialog: PairingDialog | None = None
|
||||
self._fcc_dialog: HtmlRenderer | None = None
|
||||
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
dongle_id = self._params.get("DongleId", encoding="utf-8") or "N/A"
|
||||
serial = self._params.get("HardwareSerial") or "N/A"
|
||||
|
||||
items = [
|
||||
text_item("Dongle ID", dongle_id),
|
||||
text_item("Serial", serial),
|
||||
button_item("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], callback=self._pair_device),
|
||||
button_item("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], callback=self._show_driver_camera, enabled=ui_state.is_offroad),
|
||||
button_item("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt),
|
||||
regulatory_btn := button_item("Regulatory", "VIEW", callback=self._on_regulatory),
|
||||
button_item("Review Training Guide", "REVIEW", DESCRIPTIONS['review_guide'], self._on_review_training_guide),
|
||||
button_item("Change Language", "CHANGE", callback=self._show_language_selection, enabled=ui_state.is_offroad),
|
||||
dual_button_item("Reboot", "Power Off", left_callback=self._reboot_prompt, right_callback=self._power_off_prompt),
|
||||
]
|
||||
regulatory_btn.set_visible(TICI)
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def _show_language_selection(self):
|
||||
try:
|
||||
languages_file = os.path.join(BASEDIR, "selfdrive/ui/translations/languages.json")
|
||||
with open(languages_file, encoding='utf-8') as f:
|
||||
languages = json.load(f)
|
||||
|
||||
self._select_language_dialog = MultiOptionDialog("Select a language", languages)
|
||||
gui_app.set_modal_overlay(self._select_language_dialog, callback=self._handle_language_selection)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def _handle_language_selection(self, result: int):
|
||||
if result == 1 and self._select_language_dialog:
|
||||
selected_language = self._select_language_dialog.selection
|
||||
self._params.put("LanguageSetting", selected_language)
|
||||
|
||||
self._select_language_dialog = None
|
||||
|
||||
def _show_driver_camera(self):
|
||||
if not self._driver_camera:
|
||||
self._driver_camera = DriverCameraDialog()
|
||||
|
||||
gui_app.set_modal_overlay(self._driver_camera, callback=lambda result: setattr(self, '_driver_camera', None))
|
||||
|
||||
def _reset_calibration_prompt(self):
|
||||
if ui_state.engaged:
|
||||
gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Reset Calibration"))
|
||||
return
|
||||
|
||||
gui_app.set_modal_overlay(
|
||||
lambda: confirm_dialog("Are you sure you want to reset calibration?", "Reset"),
|
||||
callback=self._reset_calibration,
|
||||
)
|
||||
|
||||
def _reset_calibration(self, result: int):
|
||||
if ui_state.engaged or result != DialogResult.CONFIRM:
|
||||
return
|
||||
|
||||
self._params.remove("CalibrationParams")
|
||||
self._params.remove("LiveTorqueParameters")
|
||||
self._params.remove("LiveParameters")
|
||||
self._params.remove("LiveParametersV2")
|
||||
self._params.remove("LiveDelay")
|
||||
self._params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def _reboot_prompt(self):
|
||||
if ui_state.engaged:
|
||||
gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Reboot"))
|
||||
return
|
||||
|
||||
gui_app.set_modal_overlay(
|
||||
lambda: confirm_dialog("Are you sure you want to reboot?", "Reboot"),
|
||||
callback=self._perform_reboot,
|
||||
)
|
||||
|
||||
def _perform_reboot(self, result: int):
|
||||
if not ui_state.engaged and result == DialogResult.CONFIRM:
|
||||
self._params.put_bool_nonblocking("DoReboot", True)
|
||||
|
||||
def _power_off_prompt(self):
|
||||
if ui_state.engaged:
|
||||
gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Power Off"))
|
||||
return
|
||||
|
||||
gui_app.set_modal_overlay(
|
||||
lambda: confirm_dialog("Are you sure you want to power off?", "Power Off"),
|
||||
callback=self._perform_power_off,
|
||||
)
|
||||
|
||||
def _perform_power_off(self, result: int):
|
||||
if not ui_state.engaged and result == DialogResult.CONFIRM:
|
||||
self._params.put_bool_nonblocking("DoShutdown", True)
|
||||
|
||||
def _pair_device(self):
|
||||
if not self._pair_device_dialog:
|
||||
self._pair_device_dialog = PairingDialog()
|
||||
gui_app.set_modal_overlay(self._pair_device_dialog, callback=lambda result: setattr(self, '_pair_device_dialog', None))
|
||||
|
||||
def _on_regulatory(self):
|
||||
if not self._fcc_dialog:
|
||||
self._fcc_dialog = HtmlRenderer(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html"))
|
||||
|
||||
gui_app.set_modal_overlay(self._fcc_dialog,
|
||||
callback=lambda result: setattr(self, '_fcc_dialog', None),
|
||||
)
|
||||
|
||||
def _on_review_training_guide(self): pass
|
||||
@@ -0,0 +1,180 @@
|
||||
import pyray as rl
|
||||
import json
|
||||
import time
|
||||
import threading
|
||||
|
||||
from openpilot.common.api import Api, api_get
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
|
||||
TITLE = "Firehose Mode"
|
||||
DESCRIPTION = (
|
||||
"openpilot learns to drive by watching humans, like you, drive.\n\n"
|
||||
+ "Firehose Mode allows you to maximize your training data uploads to improve "
|
||||
+ "openpilot's driving models. More data means bigger models, which means better Experimental Mode."
|
||||
)
|
||||
INSTRUCTIONS = (
|
||||
"For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n"
|
||||
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n"
|
||||
+ "Frequently Asked Questions\n\n"
|
||||
+ "Does it matter how or where I drive? Nope, just drive as you normally would.\n\n"
|
||||
+ "Do all of my segments get pulled in Firehose Mode? No, we selectively pull a subset of your segments.\n\n"
|
||||
+ "What's a good USB-C adapter? Any fast phone or laptop charger should be fine.\n\n"
|
||||
+ "Does it matter which software I run? Yes, only upstream openpilot (and particular forks) are able to be used for training."
|
||||
)
|
||||
|
||||
|
||||
class FirehoseLayout(Widget):
|
||||
PARAM_KEY = "ApiCache_FirehoseStats"
|
||||
GREEN = rl.Color(46, 204, 113, 255)
|
||||
RED = rl.Color(231, 76, 60, 255)
|
||||
GRAY = rl.Color(68, 68, 68, 255)
|
||||
LIGHT_GRAY = rl.Color(228, 228, 228, 255)
|
||||
UPDATE_INTERVAL = 30 # seconds
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.params = Params()
|
||||
self.segment_count = self._get_segment_count()
|
||||
self.scroll_panel = GuiScrollPanel()
|
||||
|
||||
self.running = True
|
||||
self.update_thread = threading.Thread(target=self._update_loop, daemon=True)
|
||||
self.update_thread.start()
|
||||
self.last_update_time = 0
|
||||
|
||||
def _get_segment_count(self) -> int:
|
||||
stats = self.params.get(self.PARAM_KEY, encoding='utf8')
|
||||
if not stats:
|
||||
return 0
|
||||
try:
|
||||
return int(json.loads(stats).get("firehose", 0))
|
||||
except Exception:
|
||||
cloudlog.exception(f"Failed to decode firehose stats: {stats}")
|
||||
return 0
|
||||
|
||||
def __del__(self):
|
||||
self.running = False
|
||||
if self.update_thread and self.update_thread.is_alive():
|
||||
self.update_thread.join(timeout=1.0)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# Calculate content dimensions
|
||||
content_width = rect.width - 80
|
||||
content_height = self._calculate_content_height(int(content_width))
|
||||
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
|
||||
|
||||
# Handle scrolling and render with clipping
|
||||
scroll_offset = self.scroll_panel.handle_scroll(rect, content_rect)
|
||||
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
||||
self._render_content(rect, scroll_offset)
|
||||
rl.end_scissor_mode()
|
||||
|
||||
def _calculate_content_height(self, content_width: int) -> int:
|
||||
height = 80 # Top margin
|
||||
|
||||
# Title
|
||||
height += 100 + 40
|
||||
|
||||
# Description
|
||||
desc_font = gui_app.font(FontWeight.NORMAL)
|
||||
desc_lines = wrap_text(desc_font, DESCRIPTION, 45, content_width)
|
||||
height += len(desc_lines) * 45 + 40
|
||||
|
||||
# Status section
|
||||
height += 32 # Separator
|
||||
status_text, _ = self._get_status()
|
||||
status_lines = wrap_text(gui_app.font(FontWeight.BOLD), status_text, 60, content_width)
|
||||
height += len(status_lines) * 60 + 20
|
||||
|
||||
# Contribution count (if available)
|
||||
if self.segment_count > 0:
|
||||
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
|
||||
contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 52, content_width)
|
||||
height += len(contrib_lines) * 52 + 20
|
||||
|
||||
# Instructions section
|
||||
height += 32 # Separator
|
||||
inst_lines = wrap_text(gui_app.font(FontWeight.NORMAL), INSTRUCTIONS, 40, content_width)
|
||||
height += len(inst_lines) * 40 + 40 # Bottom margin
|
||||
|
||||
return height
|
||||
|
||||
def _render_content(self, rect: rl.Rectangle, scroll_offset: rl.Vector2):
|
||||
x = int(rect.x + 40)
|
||||
y = int(rect.y + 40 + scroll_offset.y)
|
||||
w = int(rect.width - 80)
|
||||
|
||||
# Title
|
||||
title_font = gui_app.font(FontWeight.MEDIUM)
|
||||
rl.draw_text_ex(title_font, TITLE, rl.Vector2(x, y), 100, 0, rl.WHITE)
|
||||
y += 140
|
||||
|
||||
# Description
|
||||
y = self._draw_wrapped_text(x, y, w, DESCRIPTION, gui_app.font(FontWeight.NORMAL), 45, rl.WHITE)
|
||||
y += 40
|
||||
|
||||
# Separator
|
||||
rl.draw_rectangle(x, y, w, 2, self.GRAY)
|
||||
y += 30
|
||||
|
||||
# Status
|
||||
status_text, status_color = self._get_status()
|
||||
y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color)
|
||||
y += 20
|
||||
|
||||
# Contribution count (if available)
|
||||
if self.segment_count > 0:
|
||||
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
|
||||
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
|
||||
y += 20
|
||||
|
||||
# Separator
|
||||
rl.draw_rectangle(x, y, w, 2, self.GRAY)
|
||||
y += 30
|
||||
|
||||
# Instructions
|
||||
self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
|
||||
|
||||
def _draw_wrapped_text(self, x, y, width, text, font, size, color):
|
||||
wrapped = wrap_text(font, text, size, width)
|
||||
for line in wrapped:
|
||||
rl.draw_text_ex(font, line, rl.Vector2(x, y), size, 0, color)
|
||||
y += size
|
||||
return y
|
||||
|
||||
def _get_status(self) -> tuple[str, rl.Color]:
|
||||
network_type = ui_state.sm["deviceState"].networkType
|
||||
network_metered = ui_state.sm["deviceState"].networkMetered
|
||||
|
||||
if not network_metered and network_type != 0: # Not metered and connected
|
||||
return "ACTIVE", self.GREEN
|
||||
else:
|
||||
return "INACTIVE: connect to an unmetered network", self.RED
|
||||
|
||||
def _fetch_firehose_stats(self):
|
||||
try:
|
||||
dongle_id = self.params.get("DongleId", encoding='utf8')
|
||||
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
|
||||
return
|
||||
identity_token = Api(dongle_id).get_token()
|
||||
response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
self.segment_count = data.get("firehose", 0)
|
||||
self.params.put(self.PARAM_KEY, json.dumps(data))
|
||||
except Exception as e:
|
||||
cloudlog.error(f"Failed to fetch firehose stats: {e}")
|
||||
|
||||
def _update_loop(self):
|
||||
while self.running:
|
||||
if not ui_state.started:
|
||||
self._fetch_firehose_stats()
|
||||
time.sleep(self.UPDATE_INTERVAL)
|
||||
@@ -0,0 +1,156 @@
|
||||
import pyray as rl
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.selfdrive.ui.layouts.network import NetworkLayout
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
|
||||
# Import individual panels
|
||||
|
||||
SETTINGS_CLOSE_TEXT = "X"
|
||||
# Constants
|
||||
SIDEBAR_WIDTH = 500
|
||||
CLOSE_BTN_SIZE = 200
|
||||
NAV_BTN_HEIGHT = 110
|
||||
PANEL_MARGIN = 50
|
||||
|
||||
# Colors
|
||||
SIDEBAR_COLOR = rl.BLACK
|
||||
PANEL_COLOR = rl.Color(41, 41, 41, 255)
|
||||
CLOSE_BTN_COLOR = rl.Color(41, 41, 41, 255)
|
||||
CLOSE_BTN_PRESSED = rl.Color(59, 59, 59, 255)
|
||||
TEXT_NORMAL = rl.Color(128, 128, 128, 255)
|
||||
TEXT_SELECTED = rl.Color(255, 255, 255, 255)
|
||||
|
||||
|
||||
class PanelType(IntEnum):
|
||||
DEVICE = 0
|
||||
NETWORK = 1
|
||||
TOGGLES = 2
|
||||
SOFTWARE = 3
|
||||
FIREHOSE = 4
|
||||
DEVELOPER = 5
|
||||
|
||||
|
||||
@dataclass
|
||||
class PanelInfo:
|
||||
name: str
|
||||
instance: object
|
||||
button_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
|
||||
class SettingsLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._current_panel = PanelType.DEVICE
|
||||
|
||||
# Panel configuration
|
||||
self._panels = {
|
||||
PanelType.DEVICE: PanelInfo("Device", DeviceLayout()),
|
||||
PanelType.NETWORK: PanelInfo("Network", NetworkLayout()),
|
||||
PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout()),
|
||||
PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout()),
|
||||
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout()),
|
||||
PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayout()),
|
||||
}
|
||||
|
||||
self._font_medium = gui_app.font(FontWeight.MEDIUM)
|
||||
self._font_bold = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
|
||||
# Callbacks
|
||||
self._close_callback: Callable | None = None
|
||||
|
||||
def set_callbacks(self, on_close: Callable):
|
||||
self._close_callback = on_close
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# Calculate layout
|
||||
sidebar_rect = rl.Rectangle(rect.x, rect.y, SIDEBAR_WIDTH, rect.height)
|
||||
panel_rect = rl.Rectangle(rect.x + SIDEBAR_WIDTH, rect.y, rect.width - SIDEBAR_WIDTH, rect.height)
|
||||
|
||||
# Draw components
|
||||
self._draw_sidebar(sidebar_rect)
|
||||
self._draw_current_panel(panel_rect)
|
||||
|
||||
def _draw_sidebar(self, rect: rl.Rectangle):
|
||||
rl.draw_rectangle_rec(rect, SIDEBAR_COLOR)
|
||||
|
||||
# Close button
|
||||
close_btn_rect = rl.Rectangle(
|
||||
rect.x + (rect.width - CLOSE_BTN_SIZE) / 2, rect.y + 45, CLOSE_BTN_SIZE, CLOSE_BTN_SIZE
|
||||
)
|
||||
|
||||
pressed = (rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and
|
||||
rl.check_collision_point_rec(rl.get_mouse_position(), close_btn_rect))
|
||||
close_color = CLOSE_BTN_PRESSED if pressed else CLOSE_BTN_COLOR
|
||||
rl.draw_rectangle_rounded(close_btn_rect, 1.0, 20, close_color)
|
||||
|
||||
close_text_size = measure_text_cached(self._font_bold, SETTINGS_CLOSE_TEXT, 140)
|
||||
close_text_pos = rl.Vector2(
|
||||
close_btn_rect.x + (close_btn_rect.width - close_text_size.x) / 2,
|
||||
close_btn_rect.y + (close_btn_rect.height - close_text_size.y) / 2,
|
||||
)
|
||||
rl.draw_text_ex(self._font_bold, SETTINGS_CLOSE_TEXT, close_text_pos, 140, 0, TEXT_SELECTED)
|
||||
|
||||
# Store close button rect for click detection
|
||||
self._close_btn_rect = close_btn_rect
|
||||
|
||||
# Navigation buttons
|
||||
y = rect.y + 300
|
||||
for panel_type, panel_info in self._panels.items():
|
||||
button_rect = rl.Rectangle(rect.x + 50, y, rect.width - 150, NAV_BTN_HEIGHT)
|
||||
|
||||
# Button styling
|
||||
is_selected = panel_type == self._current_panel
|
||||
text_color = TEXT_SELECTED if is_selected else TEXT_NORMAL
|
||||
# Draw button text (right-aligned)
|
||||
text_size = measure_text_cached(self._font_medium, panel_info.name, 65)
|
||||
text_pos = rl.Vector2(
|
||||
button_rect.x + button_rect.width - text_size.x, button_rect.y + (button_rect.height - text_size.y) / 2
|
||||
)
|
||||
rl.draw_text_ex(self._font_medium, panel_info.name, text_pos, 65, 0, text_color)
|
||||
|
||||
# Store button rect for click detection
|
||||
panel_info.button_rect = button_rect
|
||||
|
||||
y += NAV_BTN_HEIGHT
|
||||
|
||||
def _draw_current_panel(self, rect: rl.Rectangle):
|
||||
rl.draw_rectangle_rounded(
|
||||
rl.Rectangle(rect.x + 10, rect.y + 10, rect.width - 20, rect.height - 20), 0.04, 30, PANEL_COLOR
|
||||
)
|
||||
content_rect = rl.Rectangle(rect.x + PANEL_MARGIN, rect.y + 25, rect.width - (PANEL_MARGIN * 2), rect.height - 50)
|
||||
# rl.draw_rectangle_rounded(content_rect, 0.03, 30, PANEL_COLOR)
|
||||
panel = self._panels[self._current_panel]
|
||||
if panel.instance:
|
||||
panel.instance.render(content_rect)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: rl.Vector2) -> bool:
|
||||
# Check close button
|
||||
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
|
||||
if self._close_callback:
|
||||
self._close_callback()
|
||||
return True
|
||||
|
||||
# Check navigation buttons
|
||||
for panel_type, panel_info in self._panels.items():
|
||||
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect):
|
||||
self.set_current_panel(panel_type)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def set_current_panel(self, panel_type: PanelType):
|
||||
if panel_type != self._current_panel:
|
||||
self._current_panel = panel_type
|
||||
|
||||
def close_settings(self):
|
||||
if self._close_callback:
|
||||
self._close_callback()
|
||||
@@ -0,0 +1,42 @@
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.list_view import button_item, text_item
|
||||
from openpilot.system.ui.lib.scroller import Scroller
|
||||
from openpilot.system.ui.lib.widget import Widget, DialogResult
|
||||
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog
|
||||
|
||||
|
||||
class SoftwareLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._init_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _init_items(self):
|
||||
items = [
|
||||
text_item("Current Version", ""),
|
||||
button_item("Download", "CHECK", callback=self._on_download_update),
|
||||
button_item("Install Update", "INSTALL", callback=self._on_install_update),
|
||||
button_item("Target Branch", "SELECT", callback=self._on_select_branch),
|
||||
button_item("Uninstall", "UNINSTALL", callback=self._on_uninstall),
|
||||
]
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def _on_download_update(self): pass
|
||||
def _on_install_update(self): pass
|
||||
def _on_select_branch(self): pass
|
||||
|
||||
def _on_uninstall(self):
|
||||
def handle_uninstall_confirmation(result):
|
||||
if result == DialogResult.CONFIRM:
|
||||
self._params.put_bool("DoUninstall", True)
|
||||
|
||||
gui_app.set_modal_overlay(
|
||||
lambda: confirm_dialog("Are you sure you want to uninstall?", "Uninstall"),
|
||||
callback=handle_uninstall_confirmation,
|
||||
)
|
||||
@@ -0,0 +1,88 @@
|
||||
from openpilot.system.ui.lib.list_view import multiple_button_item, toggle_item
|
||||
from openpilot.system.ui.lib.scroller import Scroller
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
from openpilot.common.params import Params
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
"OpenpilotEnabledToggle": (
|
||||
"Use the openpilot system for adaptive cruise control and lane keep driver assistance. " +
|
||||
"Your attention is required at all times to use this feature."
|
||||
),
|
||||
"DisengageOnAccelerator": "When enabled, pressing the accelerator pedal will disengage openpilot.",
|
||||
"LongitudinalPersonality": (
|
||||
"Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " +
|
||||
"In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " +
|
||||
"your steering wheel distance button."
|
||||
),
|
||||
"IsLdwEnabled": (
|
||||
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
|
||||
"without a turn signal activated while driving over 31 mph (50 km/h)."
|
||||
),
|
||||
"AlwaysOnDM": "Enable driver monitoring even when openpilot is not engaged.",
|
||||
'RecordFront': "Upload data from the driver facing camera and help improve the driver monitoring algorithm.",
|
||||
"IsMetric": "Display speed in km/h instead of mph.",
|
||||
}
|
||||
|
||||
|
||||
class TogglesLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
items = [
|
||||
toggle_item(
|
||||
"Enable openpilot",
|
||||
DESCRIPTIONS["OpenpilotEnabledToggle"],
|
||||
self._params.get_bool("OpenpilotEnabledToggle"),
|
||||
icon="chffr_wheel.png",
|
||||
),
|
||||
toggle_item(
|
||||
"Experimental Mode",
|
||||
initial_state=self._params.get_bool("ExperimentalMode"),
|
||||
icon="experimental_white.png",
|
||||
),
|
||||
toggle_item(
|
||||
"Disengage on Accelerator Pedal",
|
||||
DESCRIPTIONS["DisengageOnAccelerator"],
|
||||
self._params.get_bool("DisengageOnAccelerator"),
|
||||
icon="disengage_on_accelerator.png",
|
||||
),
|
||||
multiple_button_item(
|
||||
"Driving Personality",
|
||||
DESCRIPTIONS["LongitudinalPersonality"],
|
||||
buttons=["Aggressive", "Standard", "Relaxed"],
|
||||
button_width=255,
|
||||
callback=self._set_longitudinal_personality,
|
||||
selected_index=int(self._params.get("LongitudinalPersonality") or 0),
|
||||
icon="speed_limit.png"
|
||||
),
|
||||
toggle_item(
|
||||
"Enable Lane Departure Warnings",
|
||||
DESCRIPTIONS["IsLdwEnabled"],
|
||||
self._params.get_bool("IsLdwEnabled"),
|
||||
icon="warning.png",
|
||||
),
|
||||
toggle_item(
|
||||
"Always-On Driver Monitoring",
|
||||
DESCRIPTIONS["AlwaysOnDM"],
|
||||
self._params.get_bool("AlwaysOnDM"),
|
||||
icon="monitoring.png",
|
||||
),
|
||||
toggle_item(
|
||||
"Record and Upload Driver Camera",
|
||||
DESCRIPTIONS["RecordFront"],
|
||||
self._params.get_bool("RecordFront"),
|
||||
icon="monitoring.png",
|
||||
),
|
||||
toggle_item(
|
||||
"Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="monitoring.png"
|
||||
),
|
||||
]
|
||||
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def _set_longitudinal_personality(self, button_index: int):
|
||||
self._params.put("LongitudinalPersonality", str(button_index))
|
||||
@@ -0,0 +1,204 @@
|
||||
import pyray as rl
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Callable
|
||||
from cereal import log
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
|
||||
SIDEBAR_WIDTH = 300
|
||||
METRIC_HEIGHT = 126
|
||||
METRIC_WIDTH = 240
|
||||
METRIC_MARGIN = 30
|
||||
|
||||
SETTINGS_BTN = rl.Rectangle(50, 35, 200, 117)
|
||||
HOME_BTN = rl.Rectangle(60, 860, 180, 180)
|
||||
|
||||
ThermalStatus = log.DeviceState.ThermalStatus
|
||||
NetworkType = log.DeviceState.NetworkType
|
||||
|
||||
|
||||
# Color scheme
|
||||
class Colors:
|
||||
SIDEBAR_BG = rl.Color(57, 57, 57, 255)
|
||||
WHITE = rl.Color(255, 255, 255, 255)
|
||||
WHITE_DIM = rl.Color(255, 255, 255, 85)
|
||||
GRAY = rl.Color(84, 84, 84, 255)
|
||||
|
||||
# Status colors
|
||||
GOOD = rl.Color(255, 255, 255, 255)
|
||||
WARNING = rl.Color(218, 202, 37, 255)
|
||||
DANGER = rl.Color(201, 34, 49, 255)
|
||||
|
||||
# UI elements
|
||||
METRIC_BORDER = rl.Color(255, 255, 255, 85)
|
||||
BUTTON_NORMAL = rl.Color(255, 255, 255, 255)
|
||||
BUTTON_PRESSED = rl.Color(255, 255, 255, 166)
|
||||
|
||||
|
||||
NETWORK_TYPES = {
|
||||
NetworkType.none: "Offline",
|
||||
NetworkType.wifi: "WiFi",
|
||||
NetworkType.cell2G: "2G",
|
||||
NetworkType.cell3G: "3G",
|
||||
NetworkType.cell4G: "LTE",
|
||||
NetworkType.cell5G: "5G",
|
||||
NetworkType.ethernet: "Ethernet",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class MetricData:
|
||||
label: str
|
||||
value: str
|
||||
color: rl.Color
|
||||
|
||||
def update(self, label: str, value: str, color: rl.Color):
|
||||
self.label = label
|
||||
self.value = value
|
||||
self.color = color
|
||||
|
||||
|
||||
class Sidebar(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._net_type = NETWORK_TYPES.get(NetworkType.none)
|
||||
self._net_strength = 0
|
||||
|
||||
self._temp_status = MetricData("TEMP", "GOOD", Colors.GOOD)
|
||||
self._panda_status = MetricData("VEHICLE", "ONLINE", Colors.GOOD)
|
||||
self._connect_status = MetricData("CONNECT", "OFFLINE", Colors.WARNING)
|
||||
|
||||
self._home_img = gui_app.texture("images/button_home.png", HOME_BTN.width, HOME_BTN.height)
|
||||
self._flag_img = gui_app.texture("images/button_flag.png", HOME_BTN.width, HOME_BTN.height)
|
||||
self._settings_img = gui_app.texture("images/button_settings.png", SETTINGS_BTN.width, SETTINGS_BTN.height)
|
||||
self._font_regular = gui_app.font(FontWeight.NORMAL)
|
||||
self._font_bold = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
|
||||
# Callbacks
|
||||
self._on_settings_click: Callable | None = None
|
||||
self._on_flag_click: Callable | None = None
|
||||
|
||||
def set_callbacks(self, on_settings: Callable | None = None, on_flag: Callable | None = None):
|
||||
self._on_settings_click = on_settings
|
||||
self._on_flag_click = on_flag
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# Background
|
||||
rl.draw_rectangle_rec(rect, Colors.SIDEBAR_BG)
|
||||
|
||||
self._draw_buttons(rect)
|
||||
self._draw_network_indicator(rect)
|
||||
self._draw_metrics(rect)
|
||||
|
||||
def _update_state(self):
|
||||
sm = ui_state.sm
|
||||
if not sm.updated['deviceState']:
|
||||
return
|
||||
|
||||
device_state = sm['deviceState']
|
||||
|
||||
self._update_network_status(device_state)
|
||||
self._update_temperature_status(device_state)
|
||||
self._update_connection_status(device_state)
|
||||
self._update_panda_status()
|
||||
|
||||
def _update_network_status(self, device_state):
|
||||
self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, "Unknown")
|
||||
strength = device_state.networkStrength
|
||||
self._net_strength = max(0, min(5, strength.raw + 1)) if strength > 0 else 0
|
||||
|
||||
def _update_temperature_status(self, device_state):
|
||||
thermal_status = device_state.thermalStatus
|
||||
|
||||
if thermal_status == ThermalStatus.green:
|
||||
self._temp_status.update("TEMP", "GOOD", Colors.GOOD)
|
||||
elif thermal_status == ThermalStatus.yellow:
|
||||
self._temp_status.update("TEMP", "OK", Colors.WARNING)
|
||||
else:
|
||||
self._temp_status.update("TEMP", "HIGH", Colors.DANGER)
|
||||
|
||||
def _update_connection_status(self, device_state):
|
||||
last_ping = device_state.lastAthenaPingTime
|
||||
if last_ping == 0:
|
||||
self._connect_status.update("CONNECT", "OFFLINE", Colors.WARNING)
|
||||
elif time.monotonic_ns() - last_ping < 80_000_000_000: # 80 seconds in nanoseconds
|
||||
self._connect_status.update("CONNECT", "ONLINE", Colors.GOOD)
|
||||
else:
|
||||
self._connect_status.update("CONNECT", "ERROR", Colors.DANGER)
|
||||
|
||||
def _update_panda_status(self):
|
||||
if ui_state.panda_type == log.PandaState.PandaType.unknown:
|
||||
self._panda_status.update("NO", "PANDA", Colors.DANGER)
|
||||
else:
|
||||
self._panda_status.update("VEHICLE", "ONLINE", Colors.GOOD)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: rl.Vector2):
|
||||
if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN):
|
||||
if self._on_settings_click:
|
||||
self._on_settings_click()
|
||||
elif rl.check_collision_point_rec(mouse_pos, HOME_BTN) and ui_state.started:
|
||||
if self._on_flag_click:
|
||||
self._on_flag_click()
|
||||
|
||||
def _draw_buttons(self, rect: rl.Rectangle):
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
mouse_down = self._is_pressed and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
|
||||
def _draw_network_indicator(self, rect: rl.Rectangle):
|
||||
# Signal strength dots
|
||||
x_start = rect.x + 58
|
||||
y_pos = rect.y + 196
|
||||
dot_size = 27
|
||||
dot_spacing = 37
|
||||
|
||||
for i in range(5):
|
||||
color = Colors.WHITE if i < self._net_strength else Colors.GRAY
|
||||
x = int(x_start + i * dot_spacing + dot_size // 2)
|
||||
y = int(y_pos + dot_size // 2)
|
||||
rl.draw_circle(x, y, dot_size // 2, color)
|
||||
|
||||
# Network type text
|
||||
text_y = rect.y + 247
|
||||
text_pos = rl.Vector2(rect.x + 58, text_y)
|
||||
rl.draw_text_ex(self._font_regular, self._net_type, text_pos, 35, 0, Colors.WHITE)
|
||||
|
||||
def _draw_metrics(self, rect: rl.Rectangle):
|
||||
metrics = [(self._temp_status, 338), (self._panda_status, 496), (self._connect_status, 654)]
|
||||
|
||||
for metric, y_offset in metrics:
|
||||
self._draw_metric(rect, metric, rect.y + y_offset)
|
||||
|
||||
def _draw_metric(self, rect: rl.Rectangle, metric: MetricData, y: float):
|
||||
metric_rect = rl.Rectangle(rect.x + METRIC_MARGIN, y, METRIC_WIDTH, METRIC_HEIGHT)
|
||||
# Draw colored left edge (clipped rounded rectangle)
|
||||
edge_rect = rl.Rectangle(metric_rect.x + 4, metric_rect.y + 4, 100, 118)
|
||||
rl.begin_scissor_mode(int(metric_rect.x + 4), int(metric_rect.y), 18, int(metric_rect.height))
|
||||
rl.draw_rectangle_rounded(edge_rect, 0.18, 10, metric.color)
|
||||
rl.end_scissor_mode()
|
||||
|
||||
# Draw border
|
||||
rl.draw_rectangle_rounded_lines_ex(metric_rect, 0.15, 10, 2, Colors.METRIC_BORDER)
|
||||
|
||||
# Draw text
|
||||
text = f"{metric.label}\n{metric.value}"
|
||||
text_size = measure_text_cached(self._font_bold, text, 35)
|
||||
text_pos = rl.Vector2(
|
||||
metric_rect.x + 22 + (metric_rect.width - 22 - text_size.x) / 2,
|
||||
metric_rect.y + (metric_rect.height - text_size.y) / 2
|
||||
)
|
||||
rl.draw_text_ex(self._font_bold, text, text_pos, 35, 0, Colors.WHITE)
|
||||
@@ -0,0 +1,99 @@
|
||||
from enum import IntEnum
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
from openpilot.common.api import Api, api_get
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
|
||||
|
||||
|
||||
class PrimeType(IntEnum):
|
||||
UNKNOWN = -2,
|
||||
UNPAIRED = -1,
|
||||
NONE = 0,
|
||||
MAGENTA = 1,
|
||||
LITE = 2,
|
||||
BLUE = 3,
|
||||
MAGENTA_NEW = 4,
|
||||
PURPLE = 5,
|
||||
|
||||
|
||||
class PrimeState:
|
||||
FETCH_INTERVAL = 5.0 # seconds between API calls
|
||||
API_TIMEOUT = 10.0 # seconds for API requests
|
||||
SLEEP_INTERVAL = 0.5 # seconds to sleep between checks in the worker thread
|
||||
|
||||
def __init__(self):
|
||||
self._params = Params()
|
||||
self._lock = threading.Lock()
|
||||
self.prime_type: PrimeType = self._load_initial_state()
|
||||
|
||||
self._running = False
|
||||
self._thread = None
|
||||
self.start()
|
||||
|
||||
def _load_initial_state(self) -> PrimeType:
|
||||
prime_type_str = os.getenv("PRIME_TYPE") or self._params.get("PrimeType", encoding='utf8')
|
||||
try:
|
||||
if prime_type_str is not None:
|
||||
return PrimeType(int(prime_type_str))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return PrimeType.UNKNOWN
|
||||
|
||||
def _fetch_prime_status(self) -> None:
|
||||
dongle_id = self._params.get("DongleId", encoding='utf8')
|
||||
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
|
||||
return
|
||||
|
||||
try:
|
||||
identity_token = Api(dongle_id).get_token()
|
||||
response = api_get(f"v1.1/devices/{dongle_id}", timeout=self.API_TIMEOUT, access_token=identity_token)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
is_paired = data.get("is_paired", False)
|
||||
prime_type = data.get("prime_type", 0)
|
||||
self.set_type(PrimeType(prime_type) if is_paired else PrimeType.UNPAIRED)
|
||||
except Exception as e:
|
||||
cloudlog.error(f"Failed to fetch prime status: {e}")
|
||||
|
||||
def set_type(self, prime_type: PrimeType) -> None:
|
||||
with self._lock:
|
||||
if prime_type != self.prime_type:
|
||||
self.prime_type = prime_type
|
||||
self._params.put("PrimeType", str(int(prime_type)))
|
||||
cloudlog.info(f"Prime type updated to {prime_type}")
|
||||
|
||||
def _worker_thread(self) -> None:
|
||||
while self._running:
|
||||
self._fetch_prime_status()
|
||||
|
||||
for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)):
|
||||
if not self._running:
|
||||
break
|
||||
time.sleep(self.SLEEP_INTERVAL)
|
||||
|
||||
def start(self) -> None:
|
||||
if self._thread and self._thread.is_alive():
|
||||
return
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._worker_thread, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def stop(self) -> None:
|
||||
self._running = False
|
||||
if self._thread and self._thread.is_alive():
|
||||
self._thread.join(timeout=1.0)
|
||||
|
||||
def get_type(self) -> PrimeType:
|
||||
with self._lock:
|
||||
return self.prime_type
|
||||
|
||||
def is_prime(self) -> bool:
|
||||
with self._lock:
|
||||
return bool(self.prime_type > PrimeType.NONE)
|
||||
|
||||
def __del__(self):
|
||||
self.stop()
|
||||
@@ -0,0 +1,162 @@
|
||||
import time
|
||||
import pyray as rl
|
||||
from dataclasses import dataclass
|
||||
from cereal import messaging, log
|
||||
from openpilot.system.hardware import TICI
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_FPS
|
||||
from openpilot.system.ui.lib.label import gui_text_box
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
ALERT_MARGIN = 40
|
||||
ALERT_PADDING = 60
|
||||
ALERT_LINE_SPACING = 45
|
||||
ALERT_BORDER_RADIUS = 30
|
||||
|
||||
ALERT_FONT_SMALL = 66
|
||||
ALERT_FONT_MEDIUM = 74
|
||||
ALERT_FONT_BIG = 88
|
||||
|
||||
SELFDRIVE_STATE_TIMEOUT = 5 # Seconds
|
||||
SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 # Seconds
|
||||
|
||||
# Constants
|
||||
ALERT_COLORS = {
|
||||
log.SelfdriveState.AlertStatus.normal: rl.Color(0, 0, 0, 235), # Black
|
||||
log.SelfdriveState.AlertStatus.userPrompt: rl.Color(0xFE, 0x8C, 0x34, 235), # Orange
|
||||
log.SelfdriveState.AlertStatus.critical: rl.Color(0xC9, 0x22, 0x31, 235), # Red
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Alert:
|
||||
text1: str = ""
|
||||
text2: str = ""
|
||||
size: int = 0
|
||||
status: int = 0
|
||||
|
||||
|
||||
# Pre-defined alert instances
|
||||
ALERT_STARTUP_PENDING = Alert(
|
||||
text1="openpilot Unavailable",
|
||||
text2="Waiting to start",
|
||||
size=log.SelfdriveState.AlertSize.mid,
|
||||
status=log.SelfdriveState.AlertStatus.normal,
|
||||
)
|
||||
|
||||
ALERT_CRITICAL_TIMEOUT = Alert(
|
||||
text1="TAKE CONTROL IMMEDIATELY",
|
||||
text2="System Unresponsive",
|
||||
size=log.SelfdriveState.AlertSize.full,
|
||||
status=log.SelfdriveState.AlertStatus.critical,
|
||||
)
|
||||
|
||||
ALERT_CRITICAL_REBOOT = Alert(
|
||||
text1="System Unresponsive",
|
||||
text2="Reboot Device",
|
||||
size=log.SelfdriveState.AlertSize.full,
|
||||
status=log.SelfdriveState.AlertStatus.critical,
|
||||
)
|
||||
|
||||
|
||||
class AlertRenderer(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.font_regular: rl.Font = gui_app.font(FontWeight.NORMAL)
|
||||
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||
|
||||
def get_alert(self, sm: messaging.SubMaster) -> Alert | None:
|
||||
"""Generate the current alert based on selfdrive state."""
|
||||
ss = sm['selfdriveState']
|
||||
|
||||
# Check if selfdriveState messages have stopped arriving
|
||||
if not sm.updated['selfdriveState']:
|
||||
recv_frame = sm.recv_frame['selfdriveState']
|
||||
time_since_onroad = (sm.frame - ui_state.started_frame) / DEFAULT_FPS
|
||||
|
||||
# 1. Never received selfdriveState since going onroad
|
||||
waiting_for_startup = recv_frame < ui_state.started_frame
|
||||
if waiting_for_startup and time_since_onroad > 5:
|
||||
return ALERT_STARTUP_PENDING
|
||||
|
||||
# 2. Lost communication with selfdriveState after receiving it
|
||||
if TICI and not waiting_for_startup:
|
||||
ss_missing = time.monotonic() - sm.recv_time['selfdriveState']
|
||||
if ss_missing > SELFDRIVE_STATE_TIMEOUT:
|
||||
if ss.enabled and (ss_missing - SELFDRIVE_STATE_TIMEOUT) < SELFDRIVE_UNRESPONSIVE_TIMEOUT:
|
||||
return ALERT_CRITICAL_TIMEOUT
|
||||
return ALERT_CRITICAL_REBOOT
|
||||
|
||||
# No alert if size is none
|
||||
if ss.alertSize == 0:
|
||||
return None
|
||||
|
||||
# Return current alert
|
||||
return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize, status=ss.alertStatus)
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> bool:
|
||||
alert = self.get_alert(ui_state.sm)
|
||||
if not alert:
|
||||
return False
|
||||
|
||||
alert_rect = self._get_alert_rect(rect, alert.size)
|
||||
self._draw_background(alert_rect, alert)
|
||||
|
||||
text_rect = rl.Rectangle(
|
||||
alert_rect.x + ALERT_PADDING,
|
||||
alert_rect.y + ALERT_PADDING,
|
||||
alert_rect.width - 2 * ALERT_PADDING,
|
||||
alert_rect.height - 2 * ALERT_PADDING
|
||||
)
|
||||
self._draw_text(text_rect, alert)
|
||||
return True
|
||||
|
||||
def _get_alert_rect(self, rect: rl.Rectangle, size: int) -> rl.Rectangle:
|
||||
if size == log.SelfdriveState.AlertSize.full:
|
||||
return rect
|
||||
|
||||
height = (ALERT_FONT_MEDIUM + 2 * ALERT_PADDING if size == log.SelfdriveState.AlertSize.small else
|
||||
ALERT_FONT_BIG + ALERT_LINE_SPACING + ALERT_FONT_SMALL + 2 * ALERT_PADDING)
|
||||
|
||||
return rl.Rectangle(
|
||||
rect.x + ALERT_MARGIN,
|
||||
rect.y + rect.height - ALERT_MARGIN - height,
|
||||
rect.width - 2 * ALERT_MARGIN,
|
||||
height
|
||||
)
|
||||
|
||||
def _draw_background(self, rect: rl.Rectangle, alert: Alert) -> None:
|
||||
color = ALERT_COLORS.get(alert.status, ALERT_COLORS[log.SelfdriveState.AlertStatus.normal])
|
||||
|
||||
if alert.size != log.SelfdriveState.AlertSize.full:
|
||||
roundness = ALERT_BORDER_RADIUS / (min(rect.width, rect.height) / 2)
|
||||
rl.draw_rectangle_rounded(rect, roundness, 10, color)
|
||||
else:
|
||||
rl.draw_rectangle_rec(rect, color)
|
||||
|
||||
def _draw_text(self, rect: rl.Rectangle, alert: Alert) -> None:
|
||||
if alert.size == log.SelfdriveState.AlertSize.small:
|
||||
self._draw_centered(alert.text1, rect, self.font_bold, ALERT_FONT_MEDIUM)
|
||||
|
||||
elif alert.size == log.SelfdriveState.AlertSize.mid:
|
||||
self._draw_centered(alert.text1, rect, self.font_bold, ALERT_FONT_BIG, center_y=False)
|
||||
rect.y += ALERT_FONT_BIG + ALERT_LINE_SPACING
|
||||
self._draw_centered(alert.text2, rect, self.font_regular, ALERT_FONT_SMALL, center_y=False)
|
||||
|
||||
else:
|
||||
is_long = len(alert.text1) > 15
|
||||
font_size1 = 132 if is_long else 177
|
||||
align_ment = rl.GuiTextAlignment.TEXT_ALIGN_CENTER
|
||||
vertical_align = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE
|
||||
text_rect = rl.Rectangle(rect.x, rect.y, rect.width, rect.height // 2)
|
||||
|
||||
gui_text_box(text_rect, alert.text1, font_size1, alignment=align_ment, alignment_vertical=vertical_align, font_weight=FontWeight.BOLD)
|
||||
text_rect.y = rect.y + rect.height // 2
|
||||
gui_text_box(text_rect, alert.text2, ALERT_FONT_BIG, alignment=align_ment)
|
||||
|
||||
def _draw_centered(self, text, rect, font, font_size, center_y=True, color=rl.WHITE) -> None:
|
||||
text_size = measure_text_cached(font, text, font_size)
|
||||
x = rect.x + (rect.width - text_size.x) / 2
|
||||
y = rect.y + ((rect.height - text_size.y) / 2 if center_y else 0)
|
||||
rl.draw_text_ex(font, text, rl.Vector2(x, y), font_size, 0, color)
|
||||
+83
-48
@@ -1,37 +1,38 @@
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
from enum import Enum
|
||||
|
||||
from cereal import messaging, log
|
||||
from collections.abc import Callable
|
||||
from cereal import log
|
||||
from msgq.visionipc import VisionStreamType
|
||||
from openpilot.system.ui.onroad.alert_renderer import AlertRenderer
|
||||
from openpilot.system.ui.onroad.driver_state import DriverStateRenderer
|
||||
from openpilot.system.ui.onroad.hud_renderer import HudRenderer
|
||||
from openpilot.system.ui.onroad.model_renderer import ModelRenderer
|
||||
from openpilot.system.ui.widgets.cameraview import CameraView
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, UI_BORDER_SIZE
|
||||
from openpilot.selfdrive.ui.onroad.alert_renderer import AlertRenderer
|
||||
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
||||
from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer
|
||||
from openpilot.selfdrive.ui.onroad.model_renderer import ModelRenderer
|
||||
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame
|
||||
from openpilot.common.transformations.orientation import rot_from_euler
|
||||
|
||||
|
||||
OpState = log.SelfdriveState.OpenpilotState
|
||||
CALIBRATED = log.LiveCalibrationData.Status.calibrated
|
||||
ROAD_CAM = VisionStreamType.VISION_STREAM_ROAD
|
||||
WIDE_CAM = VisionStreamType.VISION_STREAM_WIDE_ROAD
|
||||
DEFAULT_DEVICE_CAMERA = DEVICE_CAMERAS["tici", "ar0231"]
|
||||
UI_BORDER_SIZE = 30
|
||||
|
||||
class BorderStatus(Enum):
|
||||
DISENGAGED = rl.Color(0x17, 0x33, 0x49, 0xc8) # Blue for disengaged state
|
||||
OVERRIDE = rl.Color(0x91, 0x9b, 0x95, 0xf1) # Gray for override state
|
||||
ENGAGED = rl.Color(0x17, 0x86, 0x44, 0xf1) # Green for engaged state
|
||||
BORDER_COLORS = {
|
||||
UIStatus.DISENGAGED: rl.Color(0x17, 0x33, 0x49, 0xC8), # Blue for disengaged state
|
||||
UIStatus.OVERRIDE: rl.Color(0x91, 0x9B, 0x95, 0xF1), # Gray for override state
|
||||
UIStatus.ENGAGED: rl.Color(0x17, 0x86, 0x44, 0xF1), # Green for engaged state
|
||||
}
|
||||
|
||||
WIDE_CAM_MAX_SPEED = 10.0 # m/s (22 mph)
|
||||
ROAD_CAM_MIN_SPEED = 15.0 # m/s (34 mph)
|
||||
|
||||
|
||||
class AugmentedRoadView(CameraView):
|
||||
def __init__(self, sm: messaging.SubMaster, stream_type: VisionStreamType):
|
||||
def __init__(self, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_ROAD):
|
||||
super().__init__("camerad", stream_type)
|
||||
|
||||
self.sm = sm
|
||||
self.stream_type = stream_type
|
||||
self.is_wide_camera = stream_type == VisionStreamType.VISION_STREAM_WIDE_ROAD
|
||||
self._set_placeholder_color(BORDER_COLORS[UIStatus.DISENGAGED])
|
||||
|
||||
self.device_camera: DeviceCameraConfig | None = None
|
||||
self.view_from_calib = view_frame_from_device_frame.copy()
|
||||
@@ -39,6 +40,7 @@ class AugmentedRoadView(CameraView):
|
||||
|
||||
self._last_calib_time: float = 0
|
||||
self._last_rect_dims = (0.0, 0.0)
|
||||
self._last_stream_type = stream_type
|
||||
self._cached_matrix: np.ndarray | None = None
|
||||
self._content_rect = rl.Rectangle()
|
||||
|
||||
@@ -47,7 +49,19 @@ class AugmentedRoadView(CameraView):
|
||||
self.alert_renderer = AlertRenderer()
|
||||
self.driver_state_renderer = DriverStateRenderer()
|
||||
|
||||
def render(self, rect):
|
||||
# Callbacks
|
||||
self._click_callback: Callable | None = None
|
||||
|
||||
def set_callbacks(self, on_click: Callable | None = None):
|
||||
self._click_callback = on_click
|
||||
|
||||
def _render(self, rect):
|
||||
# Only render when system is started to avoid invalid data access
|
||||
if not ui_state.started:
|
||||
return
|
||||
|
||||
self._switch_stream_if_needed(ui_state.sm)
|
||||
|
||||
# Update calibration before rendering
|
||||
self._update_calibration()
|
||||
|
||||
@@ -72,13 +86,13 @@ class AugmentedRoadView(CameraView):
|
||||
)
|
||||
|
||||
# Render the base camera view
|
||||
super().render(rect)
|
||||
super()._render(rect)
|
||||
|
||||
# Draw all UI overlays
|
||||
self.model_renderer.draw(self._content_rect, self.sm)
|
||||
self._hud_renderer.draw(self._content_rect, self.sm)
|
||||
self.alert_renderer.draw(self._content_rect, self.sm)
|
||||
self.driver_state_renderer.draw(self._content_rect, self.sm)
|
||||
self.model_renderer.render(self._content_rect)
|
||||
self._hud_renderer.render(self._content_rect)
|
||||
if not self.alert_renderer.render(self._content_rect):
|
||||
self.driver_state_renderer.render(self._content_rect)
|
||||
|
||||
# Custom UI extension point - add custom overlays here
|
||||
# Use self._content_rect for positioning within camera bounds
|
||||
@@ -86,19 +100,35 @@ class AugmentedRoadView(CameraView):
|
||||
# End clipping region
|
||||
rl.end_scissor_mode()
|
||||
|
||||
def _draw_border(self, rect: rl.Rectangle):
|
||||
state = self.sm["selfdriveState"]
|
||||
if state.state in (OpState.preEnabled, OpState.overriding):
|
||||
status = BorderStatus.OVERRIDE
|
||||
elif state.enabled:
|
||||
status = BorderStatus.ENGAGED
|
||||
else:
|
||||
status = BorderStatus.DISENGAGED
|
||||
# Handle click events if no HUD interaction occurred
|
||||
if not self._hud_renderer.handle_mouse_event():
|
||||
if self._click_callback and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||
if rl.check_collision_point_rec(rl.get_mouse_position(), self._content_rect):
|
||||
self._click_callback()
|
||||
|
||||
rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, status.value)
|
||||
def _draw_border(self, rect: rl.Rectangle):
|
||||
border_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED])
|
||||
rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, border_color)
|
||||
|
||||
def _switch_stream_if_needed(self, sm):
|
||||
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
|
||||
v_ego = sm['carState'].vEgo
|
||||
if v_ego < WIDE_CAM_MAX_SPEED:
|
||||
target = WIDE_CAM
|
||||
elif v_ego > ROAD_CAM_MIN_SPEED:
|
||||
target = ROAD_CAM
|
||||
else:
|
||||
# Hysteresis zone - keep current stream
|
||||
target = self.stream_type
|
||||
else:
|
||||
target = ROAD_CAM
|
||||
|
||||
if self.stream_type != target:
|
||||
self.switch_stream(target)
|
||||
|
||||
def _update_calibration(self):
|
||||
# Update device camera if not already set
|
||||
sm = ui_state.sm
|
||||
if not self.device_camera and sm.seen['roadCameraState'] and sm.seen['deviceState']:
|
||||
self.device_camera = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))]
|
||||
|
||||
@@ -106,7 +136,7 @@ class AugmentedRoadView(CameraView):
|
||||
if not (sm.updated["liveCalibration"] and sm.valid['liveCalibration']):
|
||||
return
|
||||
|
||||
calib = self.sm['liveCalibration']
|
||||
calib = sm['liveCalibration']
|
||||
if len(calib.rpyCalib) != 3 or calib.calStatus != CALIBRATED:
|
||||
return
|
||||
|
||||
@@ -121,18 +151,20 @@ class AugmentedRoadView(CameraView):
|
||||
|
||||
def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray:
|
||||
# Check if we can use cached matrix
|
||||
calib_time = self.sm.recv_frame['liveCalibration']
|
||||
calib_time = ui_state.sm.recv_frame['liveCalibration']
|
||||
current_dims = (self._content_rect.width, self._content_rect.height)
|
||||
if (self._last_calib_time == calib_time and
|
||||
self._last_rect_dims == current_dims and
|
||||
self._last_stream_type == self.stream_type and
|
||||
self._cached_matrix is not None):
|
||||
return self._cached_matrix
|
||||
|
||||
# Get camera configuration
|
||||
device_camera = self.device_camera or DEFAULT_DEVICE_CAMERA
|
||||
intrinsic = device_camera.ecam.intrinsics if self.is_wide_camera else device_camera.fcam.intrinsics
|
||||
calibration = self.view_from_wide_calib if self.is_wide_camera else self.view_from_calib
|
||||
zoom = 2.0 if self.is_wide_camera else 1.1
|
||||
is_wide_camera = self.stream_type == WIDE_CAM
|
||||
intrinsic = device_camera.ecam.intrinsics if is_wide_camera else device_camera.fcam.intrinsics
|
||||
calibration = self.view_from_wide_calib if is_wide_camera else self.view_from_calib
|
||||
zoom = 2.0 if is_wide_camera else 1.1
|
||||
|
||||
# Calculate transforms for vanishing point
|
||||
inf_point = np.array([1000.0, 0.0, 0.0])
|
||||
@@ -159,9 +191,10 @@ class AugmentedRoadView(CameraView):
|
||||
except (ZeroDivisionError, OverflowError):
|
||||
x_offset, y_offset = 0, 0
|
||||
|
||||
# Update cache values
|
||||
# Cache the computed transformation matrix to avoid recalculations
|
||||
self._last_calib_time = calib_time
|
||||
self._last_rect_dims = current_dims
|
||||
self._last_stream_type = self.stream_type
|
||||
self._cached_matrix = np.array([
|
||||
[zoom * 2 * cx / w, 0, -x_offset / w * 2],
|
||||
[0, zoom * 2 * cy / h, -y_offset / h * 2],
|
||||
@@ -169,9 +202,9 @@ class AugmentedRoadView(CameraView):
|
||||
])
|
||||
|
||||
video_transform = np.array([
|
||||
[zoom, 0.0, (w / 2 + x - x_offset) - (cx * zoom)],
|
||||
[0.0, zoom, (h / 2 + y - y_offset) - (cy * zoom)],
|
||||
[0.0, 0.0, 1.0]
|
||||
[zoom, 0.0, (w / 2 + x - x_offset) - (cx * zoom)],
|
||||
[0.0, zoom, (h / 2 + y - y_offset) - (cy * zoom)],
|
||||
[0.0, 0.0, 1.0]
|
||||
])
|
||||
self.model_renderer.set_transform(video_transform @ calib_transform)
|
||||
|
||||
@@ -180,13 +213,15 @@ class AugmentedRoadView(CameraView):
|
||||
|
||||
if __name__ == "__main__":
|
||||
gui_app.init_window("OnRoad Camera View")
|
||||
sm = messaging.SubMaster(["modelV2", "controlsState", "liveCalibration", "radarState", "deviceState",
|
||||
"pandaStates", "carParams", "driverMonitoringState", "carState", "driverStateV2",
|
||||
"roadCameraState", "wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan"])
|
||||
road_camera_view = AugmentedRoadView(sm, VisionStreamType.VISION_STREAM_ROAD)
|
||||
road_camera_view = AugmentedRoadView(ROAD_CAM)
|
||||
print("***press space to switch camera view***")
|
||||
try:
|
||||
for _ in gui_app.render():
|
||||
sm.update(0)
|
||||
ui_state.update()
|
||||
if rl.is_key_released(rl.KeyboardKey.KEY_SPACE):
|
||||
if WIDE_CAM in road_camera_view.available_streams:
|
||||
stream = ROAD_CAM if road_camera_view.stream_type == WIDE_CAM else WIDE_CAM
|
||||
road_camera_view.switch_stream(stream)
|
||||
road_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
finally:
|
||||
road_camera_view.close()
|
||||
@@ -1,16 +1,27 @@
|
||||
import platform
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.hardware import TICI
|
||||
from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage
|
||||
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
|
||||
CONNECTION_RETRY_INTERVAL = 0.2 # seconds between connection attempts
|
||||
|
||||
VERTEX_SHADER = """
|
||||
VERSION = """
|
||||
#version 300 es
|
||||
precision mediump float;
|
||||
"""
|
||||
if platform.system() == "Darwin":
|
||||
VERSION = """
|
||||
#version 330 core
|
||||
"""
|
||||
|
||||
|
||||
VERTEX_SHADER = VERSION + """
|
||||
in vec3 vertexPosition;
|
||||
in vec2 vertexTexCoord;
|
||||
in vec3 vertexNormal;
|
||||
@@ -40,9 +51,7 @@ if TICI:
|
||||
}
|
||||
"""
|
||||
else:
|
||||
FRAME_FRAGMENT_SHADER = """
|
||||
#version 300 es
|
||||
precision mediump float;
|
||||
FRAME_FRAGMENT_SHADER = VERSION + """
|
||||
in vec2 fragTexCoord;
|
||||
uniform sampler2D texture0;
|
||||
uniform sampler2D texture1;
|
||||
@@ -54,9 +63,21 @@ else:
|
||||
}
|
||||
"""
|
||||
|
||||
class CameraView:
|
||||
|
||||
class CameraView(Widget):
|
||||
def __init__(self, name: str, stream_type: VisionStreamType):
|
||||
self.client = VisionIpcClient(name, stream_type, False)
|
||||
super().__init__()
|
||||
self._name = name
|
||||
# Primary stream
|
||||
self.client = VisionIpcClient(name, stream_type, conflate=True)
|
||||
self._stream_type = stream_type
|
||||
self.available_streams: list[VisionStreamType] = []
|
||||
|
||||
# Target stream for switching
|
||||
self._target_client: VisionIpcClient | None = None
|
||||
self._target_stream_type: VisionStreamType | None = None
|
||||
self._switching: bool = False
|
||||
|
||||
self._texture_needs_update = True
|
||||
self.last_connection_attempt: float = 0.0
|
||||
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAME_FRAGMENT_SHADER)
|
||||
@@ -70,6 +91,8 @@ class CameraView:
|
||||
self.egl_images: dict[int, EGLImage] = {}
|
||||
self.egl_texture: rl.Texture | None = None
|
||||
|
||||
self._placeholder_color: rl.Color | None = None
|
||||
|
||||
# Initialize EGL for zero-copy rendering on TICI
|
||||
if TICI:
|
||||
if not init_egl():
|
||||
@@ -80,6 +103,30 @@ class CameraView:
|
||||
self.egl_texture = rl.load_texture_from_image(temp_image)
|
||||
rl.unload_image(temp_image)
|
||||
|
||||
def _set_placeholder_color(self, color: rl.Color):
|
||||
"""Set a placeholder color to be drawn when no frame is available."""
|
||||
self._placeholder_color = color
|
||||
|
||||
def switch_stream(self, stream_type: VisionStreamType) -> None:
|
||||
if self._stream_type == stream_type:
|
||||
return
|
||||
|
||||
if self._switching and self._target_stream_type == stream_type:
|
||||
return
|
||||
|
||||
cloudlog.debug(f'Preparing switch from {self._stream_type} to {stream_type}')
|
||||
|
||||
if self._target_client:
|
||||
del self._target_client
|
||||
|
||||
self._target_stream_type = stream_type
|
||||
self._target_client = VisionIpcClient(self._name, stream_type, conflate=True)
|
||||
self._switching = True
|
||||
|
||||
@property
|
||||
def stream_type(self) -> VisionStreamType:
|
||||
return self._stream_type
|
||||
|
||||
def close(self) -> None:
|
||||
self._clear_textures()
|
||||
|
||||
@@ -92,6 +139,8 @@ class CameraView:
|
||||
if self.shader and self.shader.id:
|
||||
rl.unload_shader(self.shader)
|
||||
|
||||
self.client = None
|
||||
|
||||
def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray:
|
||||
if not self.frame:
|
||||
return np.eye(3)
|
||||
@@ -105,13 +154,17 @@ class CameraView:
|
||||
zy = min(widget_aspect_ratio / frame_aspect_ratio, 1.0)
|
||||
|
||||
return np.array([
|
||||
[zx, 0.0, 0.0],
|
||||
[0.0, zy, 0.0],
|
||||
[0.0, 0.0, 1.0]
|
||||
[zx, 0.0, 0.0],
|
||||
[0.0, zy, 0.0],
|
||||
[0.0, 0.0, 1.0]
|
||||
])
|
||||
|
||||
def render(self, rect: rl.Rectangle):
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if self._switching:
|
||||
self._handle_switch()
|
||||
|
||||
if not self._ensure_connection():
|
||||
self._draw_placeholder(rect)
|
||||
return
|
||||
|
||||
# Try to get a new buffer without blocking
|
||||
@@ -121,6 +174,7 @@ class CameraView:
|
||||
self.frame = buffer
|
||||
|
||||
if not self.frame:
|
||||
self._draw_placeholder(rect)
|
||||
return
|
||||
|
||||
transform = self._calc_frame_matrix(rect)
|
||||
@@ -145,6 +199,10 @@ class CameraView:
|
||||
else:
|
||||
self._render_textures(src_rect, dst_rect)
|
||||
|
||||
def _draw_placeholder(self, rect: rl.Rectangle):
|
||||
if self._placeholder_color:
|
||||
rl.draw_rectangle_rec(rect, self._placeholder_color)
|
||||
|
||||
def _render_egl(self, src_rect: rl.Rectangle, dst_rect: rl.Rectangle) -> None:
|
||||
"""Render using EGL for direct buffer access"""
|
||||
if self.frame is None or self.egl_texture is None:
|
||||
@@ -181,7 +239,7 @@ class CameraView:
|
||||
# Update textures with new frame data
|
||||
if self._texture_needs_update:
|
||||
y_data = self.frame.data[: self.frame.uv_offset]
|
||||
uv_data = self.frame.data[self.frame.uv_offset :]
|
||||
uv_data = self.frame.data[self.frame.uv_offset:]
|
||||
|
||||
rl.update_texture(self.texture_y, rl.ffi.cast("void *", y_data.ctypes.data))
|
||||
rl.update_texture(self.texture_uv, rl.ffi.cast("void *", uv_data.ctypes.data))
|
||||
@@ -196,27 +254,69 @@ class CameraView:
|
||||
def _ensure_connection(self) -> bool:
|
||||
if not self.client.is_connected():
|
||||
self.frame = None
|
||||
self.available_streams.clear()
|
||||
|
||||
# Throttle connection attempts
|
||||
current_time = rl.get_time()
|
||||
if current_time - self.last_connection_attempt < CONNECTION_RETRY_INTERVAL:
|
||||
return False
|
||||
|
||||
self.last_connection_attempt = current_time
|
||||
|
||||
if not self.client.connect(False) or not self.client.num_buffers:
|
||||
return False
|
||||
|
||||
self._clear_textures()
|
||||
cloudlog.debug(f"Connected to {self._name} stream: {self._stream_type}, buffers: {self.client.num_buffers}")
|
||||
self._initialize_textures()
|
||||
self.available_streams = self.client.available_streams(self._name, block=False)
|
||||
|
||||
return True
|
||||
|
||||
def _handle_switch(self) -> None:
|
||||
"""Check if target stream is ready and switch immediately."""
|
||||
if not self._target_client or not self._switching:
|
||||
return
|
||||
|
||||
# Try to connect target if needed
|
||||
if not self._target_client.is_connected():
|
||||
if not self._target_client.connect(False) or not self._target_client.num_buffers:
|
||||
return
|
||||
|
||||
cloudlog.debug(f"Target stream connected: {self._target_stream_type}")
|
||||
|
||||
# Check if target has frames ready
|
||||
target_frame = self._target_client.recv(timeout_ms=0)
|
||||
if target_frame:
|
||||
self.frame = target_frame # Update current frame to target frame
|
||||
self._complete_switch()
|
||||
|
||||
def _complete_switch(self) -> None:
|
||||
"""Instantly switch to target stream."""
|
||||
cloudlog.debug(f"Switching to {self._target_stream_type}")
|
||||
# Clean up current resources
|
||||
if self.client:
|
||||
del self.client
|
||||
|
||||
# Switch to target
|
||||
self.client = self._target_client
|
||||
self._stream_type = self._target_stream_type
|
||||
self._texture_needs_update = True
|
||||
|
||||
# Reset state
|
||||
self._target_client = None
|
||||
self._target_stream_type = None
|
||||
self._switching = False
|
||||
|
||||
# Initialize textures for new stream
|
||||
self._initialize_textures()
|
||||
|
||||
def _initialize_textures(self):
|
||||
self._clear_textures()
|
||||
if not TICI:
|
||||
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride),
|
||||
int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE))
|
||||
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2),
|
||||
int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA))
|
||||
|
||||
return True
|
||||
|
||||
def _clear_textures(self):
|
||||
if self.texture_y and self.texture_y.id:
|
||||
rl.unload_texture(self.texture_y)
|
||||
@@ -0,0 +1,98 @@
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
from msgq.visionipc import VisionStreamType
|
||||
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
|
||||
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.label import gui_label
|
||||
|
||||
|
||||
class DriverCameraDialog(CameraView):
|
||||
def __init__(self):
|
||||
super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER)
|
||||
self.driver_state_renderer = DriverStateRenderer()
|
||||
|
||||
def _render(self, rect):
|
||||
super()._render(rect)
|
||||
|
||||
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||
return 1
|
||||
|
||||
if not self.frame:
|
||||
gui_label(
|
||||
rect,
|
||||
"camera starting",
|
||||
font_size=100,
|
||||
font_weight=FontWeight.BOLD,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
)
|
||||
return -1
|
||||
|
||||
self._draw_face_detection(rect)
|
||||
self.driver_state_renderer.render(rect)
|
||||
|
||||
return -1
|
||||
|
||||
def _draw_face_detection(self, rect: rl.Rectangle) -> None:
|
||||
driver_state = ui_state.sm["driverStateV2"]
|
||||
is_rhd = driver_state.wheelOnRightProb > 0.5
|
||||
driver_data = driver_state.rightDriverData if is_rhd else driver_state.leftDriverData
|
||||
face_detect = driver_data.faceProb > 0.7
|
||||
if not face_detect:
|
||||
return
|
||||
|
||||
# Get face position and orientation
|
||||
face_x, face_y = driver_data.facePosition
|
||||
face_std = max(driver_data.faceOrientationStd[0], driver_data.faceOrientationStd[1])
|
||||
alpha = 0.7
|
||||
if face_std > 0.15:
|
||||
alpha = max(0.7 - (face_std - 0.15) * 3.5, 0.0)
|
||||
|
||||
# use approx instead of distort_points
|
||||
# TODO: replace with distort_points
|
||||
fbox_x = int(1080.0 - 1714.0 * face_x)
|
||||
fbox_y = int(-135.0 + (504.0 + abs(face_x) * 112.0) + (1205.0 - abs(face_x) * 724.0) * face_y)
|
||||
box_size = 220
|
||||
|
||||
line_color = rl.Color(255, 255, 255, int(alpha * 255))
|
||||
rl.draw_rectangle_rounded_lines_ex(
|
||||
rl.Rectangle(fbox_x - box_size / 2, fbox_y - box_size / 2, box_size, box_size),
|
||||
35.0 / box_size / 2,
|
||||
10,
|
||||
10,
|
||||
line_color,
|
||||
)
|
||||
|
||||
def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray:
|
||||
driver_view_ratio = 2.0
|
||||
|
||||
# Get stream dimensions
|
||||
if self.frame:
|
||||
stream_width = self.frame.width
|
||||
stream_height = self.frame.height
|
||||
else:
|
||||
# Default values if frame not available
|
||||
stream_width = 1928
|
||||
stream_height = 1208
|
||||
|
||||
yscale = stream_height * driver_view_ratio / stream_width
|
||||
xscale = yscale * rect.height / rect.width * stream_width / stream_height
|
||||
|
||||
return np.array([
|
||||
[xscale, 0.0, 0.0],
|
||||
[0.0, yscale, 0.0],
|
||||
[0.0, 0.0, 1.0]
|
||||
])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
gui_app.init_window("Driver Camera View")
|
||||
|
||||
driver_camera_view = DriverCameraDialog()
|
||||
try:
|
||||
for _ in gui_app.render():
|
||||
ui_state.update()
|
||||
driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
finally:
|
||||
driver_camera_view.close()
|
||||
@@ -1,23 +1,24 @@
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
from dataclasses import dataclass
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UI_BORDER_SIZE
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
|
||||
# Default 3D coordinates for face keypoints as a NumPy array
|
||||
DEFAULT_FACE_KPTS_3D = np.array([
|
||||
[-5.98, -51.20, 8.00], [-17.64, -49.14, 8.00], [-23.81, -46.40, 8.00], [-29.98, -40.91, 8.00],
|
||||
[-32.04, -37.49, 8.00], [-34.10, -32.00, 8.00], [-36.16, -21.03, 8.00], [-36.16, 6.40, 8.00],
|
||||
[-35.47, 10.51, 8.00], [-32.73, 19.43, 8.00], [-29.30, 26.29, 8.00], [-24.50, 33.83, 8.00],
|
||||
[-19.01, 41.37, 8.00], [-14.21, 46.17, 8.00], [-12.16, 47.54, 8.00], [-4.61, 49.60, 8.00],
|
||||
[4.99, 49.60, 8.00], [12.53, 47.54, 8.00], [14.59, 46.17, 8.00], [19.39, 41.37, 8.00],
|
||||
[24.87, 33.83, 8.00], [29.67, 26.29, 8.00], [33.10, 19.43, 8.00], [35.84, 10.51, 8.00],
|
||||
[36.53, 6.40, 8.00], [36.53, -21.03, 8.00], [34.47, -32.00, 8.00], [32.42, -37.49, 8.00],
|
||||
[30.36, -40.91, 8.00], [24.19, -46.40, 8.00], [18.02, -49.14, 8.00], [6.36, -51.20, 8.00],
|
||||
[-5.98, -51.20, 8.00],
|
||||
[-5.98, -51.20, 8.00], [-17.64, -49.14, 8.00], [-23.81, -46.40, 8.00], [-29.98, -40.91, 8.00],
|
||||
[-32.04, -37.49, 8.00], [-34.10, -32.00, 8.00], [-36.16, -21.03, 8.00], [-36.16, 6.40, 8.00],
|
||||
[-35.47, 10.51, 8.00], [-32.73, 19.43, 8.00], [-29.30, 26.29, 8.00], [-24.50, 33.83, 8.00],
|
||||
[-19.01, 41.37, 8.00], [-14.21, 46.17, 8.00], [-12.16, 47.54, 8.00], [-4.61, 49.60, 8.00],
|
||||
[4.99, 49.60, 8.00], [12.53, 47.54, 8.00], [14.59, 46.17, 8.00], [19.39, 41.37, 8.00],
|
||||
[24.87, 33.83, 8.00], [29.67, 26.29, 8.00], [33.10, 19.43, 8.00], [35.84, 10.51, 8.00],
|
||||
[36.53, 6.40, 8.00], [36.53, -21.03, 8.00], [34.47, -32.00, 8.00], [32.42, -37.49, 8.00],
|
||||
[30.36, -40.91, 8.00], [24.19, -46.40, 8.00], [18.02, -49.14, 8.00], [6.36, -51.20, 8.00],
|
||||
[-5.98, -51.20, 8.00],
|
||||
], dtype=np.float32)
|
||||
|
||||
# UI constants
|
||||
UI_BORDER_SIZE = 30
|
||||
BTN_SIZE = 192
|
||||
IMG_SIZE = 144
|
||||
ARC_LENGTH = 133
|
||||
@@ -27,6 +28,10 @@ ARC_THICKNESS_EXTEND = 12.0
|
||||
SCALES_POS = np.array([0.9, 0.4, 0.4], dtype=np.float32)
|
||||
SCALES_NEG = np.array([0.7, 0.4, 0.4], dtype=np.float32)
|
||||
|
||||
ARC_POINT_COUNT = 37 # Number of points in the arc
|
||||
ARC_ANGLES = np.linspace(0.0, np.pi, ARC_POINT_COUNT, dtype=np.float32)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArcData:
|
||||
"""Data structure for arc rendering parameters."""
|
||||
@@ -36,14 +41,15 @@ class ArcData:
|
||||
height: float
|
||||
thickness: float
|
||||
|
||||
class DriverStateRenderer:
|
||||
|
||||
class DriverStateRenderer(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Initial state with NumPy arrays
|
||||
self.face_kpts_draw = DEFAULT_FACE_KPTS_3D.copy()
|
||||
self.is_active = False
|
||||
self.is_rhd = False
|
||||
self.dm_fade_state = 0.0
|
||||
self.state_updated = False
|
||||
self.last_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
|
||||
self.driver_pose_vals = np.zeros(3, dtype=np.float32)
|
||||
self.driver_pose_diff = np.zeros(3, dtype=np.float32)
|
||||
@@ -57,8 +63,8 @@ class DriverStateRenderer:
|
||||
|
||||
# Pre-allocate drawing arrays
|
||||
self.face_lines = [rl.Vector2(0, 0) for _ in range(len(DEFAULT_FACE_KPTS_3D))]
|
||||
self.h_arc_lines = [rl.Vector2(0, 0) for _ in range(37)] # 37 points for horizontal arc
|
||||
self.v_arc_lines = [rl.Vector2(0, 0) for _ in range(37)] # 37 points for vertical arc
|
||||
self.h_arc_lines = [rl.Vector2(0, 0) for _ in range(ARC_POINT_COUNT)]
|
||||
self.v_arc_lines = [rl.Vector2(0, 0) for _ in range(ARC_POINT_COUNT)]
|
||||
|
||||
# Load the driver face icon
|
||||
self.dm_img = gui_app.texture("icons/driver_face.png", IMG_SIZE, IMG_SIZE)
|
||||
@@ -69,14 +75,10 @@ class DriverStateRenderer:
|
||||
self.engaged_color = rl.Color(26, 242, 66, 255)
|
||||
self.disengaged_color = rl.Color(139, 139, 139, 255)
|
||||
|
||||
def draw(self, rect, sm):
|
||||
if not self._is_visible(sm):
|
||||
return
|
||||
|
||||
self._update_state(sm, rect)
|
||||
if not self.state_updated:
|
||||
return
|
||||
self.set_visible(lambda: (ui_state.sm.recv_frame['driverStateV2'] > ui_state.started_frame and
|
||||
ui_state.sm.seen['driverMonitoringState']))
|
||||
|
||||
def _render(self, rect):
|
||||
# Set opacity based on active state
|
||||
opacity = 0.65 if self.is_active else 0.2
|
||||
|
||||
@@ -92,8 +94,7 @@ class DriverStateRenderer:
|
||||
rl.draw_spline_linear(self.face_lines, len(self.face_lines), 5.2, self.white_color)
|
||||
|
||||
# Set arc color based on engaged state
|
||||
engaged = True
|
||||
self.arc_color = self.engaged_color if engaged else self.disengaged_color
|
||||
self.arc_color = self.engaged_color if ui_state.engaged else self.disengaged_color
|
||||
self.arc_color.a = int(0.4 * 255 * (1.0 - self.dm_fade_state)) # Fade out when inactive
|
||||
|
||||
# Draw arcs
|
||||
@@ -102,18 +103,14 @@ class DriverStateRenderer:
|
||||
if self.v_arc_data:
|
||||
rl.draw_spline_linear(self.v_arc_lines, len(self.v_arc_lines), self.v_arc_data.thickness, self.arc_color)
|
||||
|
||||
def _is_visible(self, sm):
|
||||
"""Check if the visualization should be rendered."""
|
||||
return (sm.seen['driverStateV2'] and
|
||||
sm.seen['driverMonitoringState'] and
|
||||
sm['selfdriveState'].alertSize == 0)
|
||||
|
||||
def _update_state(self, sm, rect):
|
||||
def _update_state(self):
|
||||
"""Update the driver monitoring state based on model data"""
|
||||
if not sm.updated["driverMonitoringState"]:
|
||||
if self.state_updated and (rect.x != self.last_rect.x or rect.y != self.last_rect.y or \
|
||||
rect.width != self.last_rect.width or rect.height != self.last_rect.height):
|
||||
self._pre_calculate_drawing_elements(rect)
|
||||
sm = ui_state.sm
|
||||
if not sm.updated["driverMonitoringState"]:
|
||||
if (self._rect.x != self.last_rect.x or self._rect.y != self.last_rect.y or
|
||||
self._rect.width != self.last_rect.width or self._rect.height != self.last_rect.height):
|
||||
self._pre_calculate_drawing_elements()
|
||||
self.last_rect = self._rect
|
||||
return
|
||||
|
||||
# Get monitoring state
|
||||
@@ -162,16 +159,15 @@ class DriverStateRenderer:
|
||||
self.face_keypoints_transformed = self.face_kpts_draw[:, :2] * kp_depth[:, None]
|
||||
|
||||
# Pre-calculate all drawing elements
|
||||
self._pre_calculate_drawing_elements(rect)
|
||||
self.state_updated = True
|
||||
self._pre_calculate_drawing_elements()
|
||||
|
||||
def _pre_calculate_drawing_elements(self, rect):
|
||||
def _pre_calculate_drawing_elements(self):
|
||||
"""Pre-calculate all drawing elements based on the current rectangle"""
|
||||
# Calculate icon position (bottom-left or bottom-right)
|
||||
width, height = rect.width, rect.height
|
||||
width, height = self._rect.width, self._rect.height
|
||||
offset = UI_BORDER_SIZE + BTN_SIZE // 2
|
||||
self.position_x = rect.x + (width - offset if self.is_rhd else offset)
|
||||
self.position_y = rect.y + height - offset
|
||||
self.position_x = self._rect.x + (width - offset if self.is_rhd else offset)
|
||||
self.position_y = self._rect.y + height - offset
|
||||
|
||||
# Pre-calculate the face lines positions
|
||||
positioned_keypoints = self.face_keypoints_transformed + np.array([self.position_x, self.position_y])
|
||||
@@ -186,15 +182,15 @@ class DriverStateRenderer:
|
||||
# Horizontal arc
|
||||
h_width = abs(delta_x)
|
||||
self.h_arc_data = self._calculate_arc_data(
|
||||
delta_x, h_width, self.position_x, self.position_y - ARC_LENGTH / 2,
|
||||
self.driver_pose_sins[1], self.driver_pose_diff[1], is_horizontal=True
|
||||
delta_x, h_width, self.position_x, self.position_y - ARC_LENGTH / 2,
|
||||
self.driver_pose_sins[1], self.driver_pose_diff[1], is_horizontal=True
|
||||
)
|
||||
|
||||
# Vertical arc
|
||||
v_height = abs(delta_y)
|
||||
self.v_arc_data = self._calculate_arc_data(
|
||||
delta_y, v_height, self.position_x - ARC_LENGTH / 2, self.position_y,
|
||||
self.driver_pose_sins[0], self.driver_pose_diff[0], is_horizontal=False
|
||||
delta_y, v_height, self.position_x - ARC_LENGTH / 2, self.position_y,
|
||||
self.driver_pose_sins[0], self.driver_pose_diff[0], is_horizontal=False
|
||||
)
|
||||
|
||||
def _calculate_arc_data(
|
||||
@@ -218,9 +214,7 @@ class DriverStateRenderer:
|
||||
)
|
||||
|
||||
# Pre-calculate arc points
|
||||
start_rad = np.deg2rad(start_angle)
|
||||
end_rad = np.deg2rad(start_angle + 180)
|
||||
angles = np.linspace(start_rad, end_rad, 37)
|
||||
angles = ARC_ANGLES + np.deg2rad(start_angle)
|
||||
|
||||
center_x = x + arc_data.width / 2
|
||||
center_y = y + arc_data.height / 2
|
||||
@@ -0,0 +1,78 @@
|
||||
import time
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
from openpilot.common.params import Params
|
||||
|
||||
|
||||
class ExpButton(Widget):
|
||||
def __init__(self, button_size: int, icon_size: int):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._experimental_mode: bool = False
|
||||
self._engageable: bool = False
|
||||
|
||||
# State hold mechanism
|
||||
self._hold_duration = 2.0 # seconds
|
||||
self._held_mode: bool | None = None
|
||||
self._hold_end_time: float | None = None
|
||||
|
||||
self._white_color: rl.Color = rl.Color(255, 255, 255, 255)
|
||||
self._black_bg: rl.Color = rl.Color(0, 0, 0, 166)
|
||||
self._txt_wheel: rl.Texture = gui_app.texture('icons/chffr_wheel.png', icon_size, icon_size)
|
||||
self._txt_exp: rl.Texture = gui_app.texture('icons/experimental.png', icon_size, icon_size)
|
||||
self._rect = rl.Rectangle(0, 0, button_size, button_size)
|
||||
|
||||
def set_rect(self, rect: rl.Rectangle) -> None:
|
||||
self._rect.x, self._rect.y = rect.x, rect.y
|
||||
|
||||
def _update_state(self) -> None:
|
||||
selfdrive_state = ui_state.sm["selfdriveState"]
|
||||
self._experimental_mode = selfdrive_state.experimentalMode
|
||||
self._engageable = selfdrive_state.engageable or selfdrive_state.enabled
|
||||
|
||||
def handle_mouse_event(self) -> bool:
|
||||
if rl.check_collision_point_rec(rl.get_mouse_position(), self._rect):
|
||||
if (rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and
|
||||
self._is_toggle_allowed()):
|
||||
new_mode = not self._experimental_mode
|
||||
self._params.put_bool("ExperimentalMode", new_mode)
|
||||
|
||||
# Hold new state temporarily
|
||||
self._held_mode = new_mode
|
||||
self._hold_end_time = time.time() + self._hold_duration
|
||||
return True
|
||||
return False
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
center_x = int(self._rect.x + self._rect.width // 2)
|
||||
center_y = int(self._rect.y + self._rect.height // 2)
|
||||
|
||||
mouse_over = rl.check_collision_point_rec(rl.get_mouse_position(), self._rect)
|
||||
mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._is_pressed
|
||||
self._white_color.a = 180 if (mouse_down and mouse_over) or not self._engageable else 255
|
||||
|
||||
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)
|
||||
|
||||
def _held_or_actual_mode(self):
|
||||
now = time.time()
|
||||
if self._hold_end_time and now < self._hold_end_time:
|
||||
return self._held_mode
|
||||
|
||||
if self._hold_end_time and now >= self._hold_end_time:
|
||||
self._hold_end_time = self._held_mode = None
|
||||
|
||||
return self._experimental_mode
|
||||
|
||||
def _is_toggle_allowed(self):
|
||||
if not self._params.get_bool("ExperimentalModeConfirmed"):
|
||||
return False
|
||||
|
||||
car_params = ui_state.sm["carParams"]
|
||||
if car_params.alphaLongitudinalAvailable:
|
||||
return self._params.get_bool("AlphaLongitudinalEnabled")
|
||||
else:
|
||||
return car_params.openpilotLongitudinalControl
|
||||
@@ -1,13 +1,16 @@
|
||||
import pyray as rl
|
||||
from dataclasses import dataclass
|
||||
from cereal.messaging import SubMaster
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.selfdrive.ui.onroad.exp_button import ExpButton
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from enum import IntEnum
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
|
||||
# Constants
|
||||
SET_SPEED_NA = 255
|
||||
KM_TO_MILE = 0.621371
|
||||
CRUISE_DISABLED_CHAR = '–'
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -52,34 +55,26 @@ FONT_SIZES = FontSizes()
|
||||
COLORS = Colors()
|
||||
|
||||
|
||||
class HudStatus(IntEnum):
|
||||
DISENGAGED = 0
|
||||
OVERRIDE = 1
|
||||
ENGAGED = 2
|
||||
|
||||
|
||||
class HudRenderer:
|
||||
class HudRenderer(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
"""Initialize the HUD renderer."""
|
||||
self.is_metric: bool = False
|
||||
self.status: HudStatus = HudStatus.DISENGAGED
|
||||
self.is_cruise_set: bool = False
|
||||
self.is_cruise_available: bool = False
|
||||
self.set_speed: float = SET_SPEED_NA
|
||||
self.speed: float = 0.0
|
||||
self.v_ego_cluster_seen: bool = False
|
||||
self.font_metrics_cache: dict[[str, int, str], rl.Vector2] = {}
|
||||
self._wheel_texture: rl.Texture = gui_app.texture('icons/chffr_wheel.png', UI_CONFIG.wheel_icon_size, UI_CONFIG.wheel_icon_size)
|
||||
|
||||
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
def _update_state(self, sm: SubMaster) -> None:
|
||||
"""Update HUD state based on car state and controls state."""
|
||||
self.is_metric = True
|
||||
self.status = HudStatus.DISENGAGED
|
||||
self._exp_button = ExpButton(UI_CONFIG.button_size, UI_CONFIG.wheel_icon_size)
|
||||
|
||||
if not sm.valid['carState']:
|
||||
def _update_state(self) -> None:
|
||||
"""Update HUD state based on car state and controls state."""
|
||||
sm = ui_state.sm
|
||||
if sm.recv_frame["carState"] < ui_state.started_frame:
|
||||
self.is_cruise_set = False
|
||||
self.set_speed = SET_SPEED_NA
|
||||
self.speed = 0.0
|
||||
@@ -95,18 +90,18 @@ class HudRenderer:
|
||||
self.is_cruise_set = 0 < self.set_speed < SET_SPEED_NA
|
||||
self.is_cruise_available = self.set_speed != -1
|
||||
|
||||
if self.is_cruise_set and not self.is_metric:
|
||||
if self.is_cruise_set and not ui_state.is_metric:
|
||||
self.set_speed *= KM_TO_MILE
|
||||
|
||||
v_ego_cluster = car_state.vEgoCluster
|
||||
self.v_ego_cluster_seen = self.v_ego_cluster_seen or v_ego_cluster != 0.0
|
||||
v_ego = v_ego_cluster if self.v_ego_cluster_seen else car_state.vEgo
|
||||
speed_conversion = CV.MS_TO_KPH if self.is_metric else CV.MS_TO_MPH
|
||||
speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||
self.speed = max(0.0, v_ego * speed_conversion)
|
||||
|
||||
def draw(self, rect: rl.Rectangle, sm: SubMaster) -> None:
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
"""Render HUD elements to the screen."""
|
||||
self._update_state(sm)
|
||||
# Draw the header background
|
||||
rl.draw_rectangle_gradient_v(
|
||||
int(rect.x),
|
||||
int(rect.y),
|
||||
@@ -120,11 +115,17 @@ class HudRenderer:
|
||||
self._draw_set_speed(rect)
|
||||
|
||||
self._draw_current_speed(rect)
|
||||
self._draw_wheel_icon(rect)
|
||||
|
||||
button_x = rect.x + rect.width - UI_CONFIG.border_size - UI_CONFIG.button_size
|
||||
button_y = rect.y + UI_CONFIG.border_size
|
||||
self._exp_button.render(rl.Rectangle(button_x, button_y, UI_CONFIG.button_size, UI_CONFIG.button_size))
|
||||
|
||||
def handle_mouse_event(self) -> bool:
|
||||
return bool(self._exp_button.handle_mouse_event())
|
||||
|
||||
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
|
||||
"""Draw the MAX speed indicator box."""
|
||||
set_speed_width = UI_CONFIG.set_speed_width_metric if self.is_metric else UI_CONFIG.set_speed_width_imperial
|
||||
set_speed_width = UI_CONFIG.set_speed_width_metric if ui_state.is_metric else UI_CONFIG.set_speed_width_imperial
|
||||
x = rect.x + 60 + (UI_CONFIG.set_speed_width_imperial - set_speed_width) // 2
|
||||
y = rect.y + 45
|
||||
|
||||
@@ -136,14 +137,15 @@ class HudRenderer:
|
||||
set_speed_color = COLORS.dark_grey
|
||||
if self.is_cruise_set:
|
||||
set_speed_color = COLORS.white
|
||||
max_color = {
|
||||
HudStatus.DISENGAGED: COLORS.disengaged,
|
||||
HudStatus.OVERRIDE: COLORS.override,
|
||||
HudStatus.ENGAGED: COLORS.engaged,
|
||||
}.get(self.status, COLORS.grey)
|
||||
if ui_state.status == UIStatus.ENGAGED:
|
||||
max_color = COLORS.engaged
|
||||
elif ui_state.status == UIStatus.DISENGAGED:
|
||||
max_color = COLORS.disengaged
|
||||
elif ui_state.status == UIStatus.OVERRIDE:
|
||||
max_color = COLORS.override
|
||||
|
||||
max_text = "MAX"
|
||||
max_text_width = self._measure_text(max_text, self._font_semi_bold, FONT_SIZES.max_speed, 'semi_bold').x
|
||||
max_text_width = measure_text_cached(self._font_semi_bold, max_text, FONT_SIZES.max_speed).x
|
||||
rl.draw_text_ex(
|
||||
self._font_semi_bold,
|
||||
max_text,
|
||||
@@ -153,8 +155,8 @@ class HudRenderer:
|
||||
max_color,
|
||||
)
|
||||
|
||||
set_speed_text = "–" if not self.is_cruise_set else str(round(self.set_speed))
|
||||
speed_text_width = self._measure_text(set_speed_text, self._font_bold, FONT_SIZES.set_speed, 'bold').x
|
||||
set_speed_text = CRUISE_DISABLED_CHAR if not self.is_cruise_set else str(round(self.set_speed))
|
||||
speed_text_width = measure_text_cached(self._font_bold, set_speed_text, FONT_SIZES.set_speed).x
|
||||
rl.draw_text_ex(
|
||||
self._font_bold,
|
||||
set_speed_text,
|
||||
@@ -167,28 +169,11 @@ class HudRenderer:
|
||||
def _draw_current_speed(self, rect: rl.Rectangle) -> None:
|
||||
"""Draw the current vehicle speed and unit."""
|
||||
speed_text = str(round(self.speed))
|
||||
speed_text_size = self._measure_text(speed_text, self._font_bold, FONT_SIZES.current_speed, 'bold')
|
||||
speed_text_size = measure_text_cached(self._font_bold, speed_text, FONT_SIZES.current_speed)
|
||||
speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2)
|
||||
rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white)
|
||||
|
||||
unit_text = "km/h" if self.is_metric else "mph"
|
||||
unit_text_size = self._measure_text(unit_text, self._font_medium, FONT_SIZES.speed_unit, 'medium')
|
||||
unit_text = "km/h" if ui_state.is_metric else "mph"
|
||||
unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit)
|
||||
unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2)
|
||||
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.white_translucent)
|
||||
|
||||
def _draw_wheel_icon(self, rect: rl.Rectangle) -> None:
|
||||
"""Draw the steering wheel icon with status-based opacity."""
|
||||
center_x = int(rect.x + rect.width - UI_CONFIG.border_size - UI_CONFIG.button_size / 2)
|
||||
center_y = int(rect.y + UI_CONFIG.border_size + UI_CONFIG.button_size / 2)
|
||||
rl.draw_circle(center_x, center_y, UI_CONFIG.button_size / 2, COLORS.black_translucent)
|
||||
|
||||
opacity = 0.7 if self.status == HudStatus.DISENGAGED else 1.0
|
||||
img_pos = rl.Vector2(center_x - self._wheel_texture.width / 2, center_y - self._wheel_texture.height / 2)
|
||||
rl.draw_texture_v(self._wheel_texture, img_pos, rl.Color(255, 255, 255, int(255 * opacity)))
|
||||
|
||||
def _measure_text(self, text: str, font: rl.Font, font_size: int, font_type: str) -> rl.Vector2:
|
||||
"""Measure text dimensions with caching."""
|
||||
key = (text, font_size, font_type)
|
||||
if key not in self.font_metrics_cache:
|
||||
self.font_metrics_cache[key] = rl.measure_text_ex(font, text, font_size, 0)
|
||||
return self.font_metrics_cache[key]
|
||||
@@ -4,9 +4,11 @@ import pyray as rl
|
||||
from cereal import messaging, car
|
||||
from dataclasses import dataclass, field
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import DEFAULT_FPS
|
||||
from openpilot.system.ui.lib.shader_polygon import draw_polygon
|
||||
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT
|
||||
|
||||
CLIP_MARGIN = 500
|
||||
MIN_DRAW_DISTANCE = 10.0
|
||||
@@ -14,6 +16,8 @@ MAX_DRAW_DISTANCE = 100.0
|
||||
PATH_COLOR_TRANSITION_DURATION = 0.5 # Seconds for color transition animation
|
||||
PATH_BLEND_INCREMENT = 1.0 / (PATH_COLOR_TRANSITION_DURATION * DEFAULT_FPS)
|
||||
|
||||
MAX_POINTS = 200
|
||||
|
||||
THROTTLE_COLORS = [
|
||||
rl.Color(13, 248, 122, 102), # HSLF(148/360, 0.94, 0.51, 0.4)
|
||||
rl.Color(114, 255, 92, 89), # HSLF(112/360, 1.0, 0.68, 0.35)
|
||||
@@ -32,6 +36,7 @@ class ModelPoints:
|
||||
raw_points: np.ndarray = field(default_factory=lambda: np.empty((0, 3), dtype=np.float32))
|
||||
projected_points: np.ndarray = field(default_factory=lambda: np.empty((0, 2), dtype=np.float32))
|
||||
|
||||
|
||||
@dataclass
|
||||
class LeadVehicle:
|
||||
glow: list[float] = field(default_factory=list)
|
||||
@@ -39,8 +44,9 @@ class LeadVehicle:
|
||||
fill_alpha: int = 0
|
||||
|
||||
|
||||
class ModelRenderer:
|
||||
class ModelRenderer(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._longitudinal_control = False
|
||||
self._experimental_mode = False
|
||||
self._blend_factor = 1.0
|
||||
@@ -48,7 +54,7 @@ class ModelRenderer:
|
||||
self._lane_line_probs = np.zeros(4, dtype=np.float32)
|
||||
self._road_edge_stds = np.zeros(2, dtype=np.float32)
|
||||
self._lead_vehicles = [LeadVehicle(), LeadVehicle()]
|
||||
self._path_offset_z = 1.22
|
||||
self._path_offset_z = HEIGHT_INIT[0]
|
||||
|
||||
# Initialize ModelPoints objects
|
||||
self._path = ModelPoints()
|
||||
@@ -60,7 +66,7 @@ class ModelRenderer:
|
||||
self._car_space_transform = np.zeros((3, 3), dtype=np.float32)
|
||||
self._transform_dirty = True
|
||||
self._clip_region = None
|
||||
self._rect = None
|
||||
|
||||
self._exp_gradient = {
|
||||
'start': (0.0, 1.0), # Bottom of path
|
||||
'end': (0.0, 0.0), # Top of path
|
||||
@@ -77,20 +83,25 @@ class ModelRenderer:
|
||||
self._car_space_transform = transform.astype(np.float32)
|
||||
self._transform_dirty = True
|
||||
|
||||
def draw(self, rect: rl.Rectangle, sm: messaging.SubMaster):
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
sm = ui_state.sm
|
||||
|
||||
# Check if data is up-to-date
|
||||
if not sm.valid['modelV2'] or not sm.valid['liveCalibration']:
|
||||
if (sm.recv_frame["liveCalibration"] < ui_state.started_frame or
|
||||
sm.recv_frame["modelV2"] < ui_state.started_frame):
|
||||
return
|
||||
|
||||
# Set up clipping region
|
||||
self._rect = rect
|
||||
self._clip_region = rl.Rectangle(
|
||||
rect.x - CLIP_MARGIN, rect.y - CLIP_MARGIN, rect.width + 2 * CLIP_MARGIN, rect.height + 2 * CLIP_MARGIN
|
||||
)
|
||||
|
||||
# Update state
|
||||
self._experimental_mode = sm['selfdriveState'].experimentalMode
|
||||
self._path_offset_z = sm['liveCalibration'].height[0]
|
||||
|
||||
live_calib = sm['liveCalibration']
|
||||
self._path_offset_z = live_calib.height[0] if live_calib.height else HEIGHT_INIT[0]
|
||||
|
||||
if sm.updated['carParams']:
|
||||
self._longitudinal_control = sm['carParams'].openpilotLongitudinalControl
|
||||
|
||||
@@ -105,16 +116,15 @@ class ModelRenderer:
|
||||
if model_updated:
|
||||
self._update_raw_points(model)
|
||||
|
||||
pos_x_array = self._path.raw_points[:, 0]
|
||||
if pos_x_array.size == 0:
|
||||
path_x_array = self._path.raw_points[:, 0]
|
||||
if path_x_array.size == 0:
|
||||
return
|
||||
|
||||
self._update_model(lead_one, pos_x_array)
|
||||
self._update_model(lead_one, path_x_array)
|
||||
if render_lead_indicator:
|
||||
self._update_leads(radar_state, pos_x_array)
|
||||
self._update_leads(radar_state, path_x_array)
|
||||
self._transform_dirty = False
|
||||
|
||||
|
||||
# Draw elements
|
||||
self._draw_lane_lines()
|
||||
self._draw_path(sm)
|
||||
@@ -136,23 +146,26 @@ class ModelRenderer:
|
||||
self._road_edge_stds = np.array(model.roadEdgeStds, dtype=np.float32)
|
||||
self._acceleration_x = np.array(model.acceleration.x, dtype=np.float32)
|
||||
|
||||
def _update_leads(self, radar_state, pos_x_array):
|
||||
def _update_leads(self, radar_state, path_x_array):
|
||||
"""Update positions of lead vehicles"""
|
||||
self._lead_vehicles = [LeadVehicle(), LeadVehicle()]
|
||||
leads = [radar_state.leadOne, radar_state.leadTwo]
|
||||
|
||||
for i, lead_data in enumerate(leads):
|
||||
if lead_data and lead_data.status:
|
||||
d_rel, y_rel, v_rel = lead_data.dRel, lead_data.yRel, lead_data.vRel
|
||||
idx = self._get_path_length_idx(pos_x_array, d_rel)
|
||||
idx = self._get_path_length_idx(path_x_array, d_rel)
|
||||
|
||||
# Get z-coordinate from path at the lead vehicle position
|
||||
z = self._path.raw_points[idx, 2] if idx < len(self._path.raw_points) else 0.0
|
||||
point = self._map_to_screen(d_rel, -y_rel, z + self._path_offset_z)
|
||||
if point:
|
||||
self._lead_vehicles[i] = self._update_lead_vehicle(d_rel, v_rel, point, self._rect)
|
||||
|
||||
def _update_model(self, lead, pos_x_array):
|
||||
def _update_model(self, lead, path_x_array):
|
||||
"""Update model visualization data based on model message"""
|
||||
max_distance = np.clip(pos_x_array[-1], MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE)
|
||||
max_idx = self._get_path_length_idx(pos_x_array, max_distance)
|
||||
max_distance = np.clip(path_x_array[-1], MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE)
|
||||
max_idx = self._get_path_length_idx(self._lane_lines[0].raw_points[:, 0], max_distance)
|
||||
|
||||
# Update lane lines using raw points
|
||||
for i, lane_line in enumerate(self._lane_lines):
|
||||
@@ -168,8 +181,8 @@ class ModelRenderer:
|
||||
if lead and lead.status:
|
||||
lead_d = lead.dRel * 2.0
|
||||
max_distance = np.clip(lead_d - min(lead_d * 0.35, 10.0), 0.0, max_distance)
|
||||
max_idx = self._get_path_length_idx(pos_x_array, max_distance)
|
||||
|
||||
max_idx = self._get_path_length_idx(path_x_array, max_distance)
|
||||
self._path.projected_points = self._map_line_to_polygon(
|
||||
self._path.raw_points, 0.9, self._path_offset_z, max_idx, allow_invert=False
|
||||
)
|
||||
@@ -240,7 +253,7 @@ class ModelRenderer:
|
||||
glow = [(x + (sz * 1.35) + g_xo, y + sz + g_yo), (x, y - g_yo), (x - (sz * 1.35) - g_xo, y + sz + g_yo)]
|
||||
chevron = [(x + (sz * 1.25), y + sz), (x, y), (x - (sz * 1.25), y + sz)]
|
||||
|
||||
return LeadVehicle(glow=glow,chevron=chevron, fill_alpha=int(fill_alpha))
|
||||
return LeadVehicle(glow=glow, chevron=chevron, fill_alpha=int(fill_alpha))
|
||||
|
||||
def _draw_lane_lines(self):
|
||||
"""Draw lane lines and road edges"""
|
||||
@@ -309,8 +322,10 @@ class ModelRenderer:
|
||||
@staticmethod
|
||||
def _get_path_length_idx(pos_x_array: np.ndarray, path_height: float) -> int:
|
||||
"""Get the index corresponding to the given path height"""
|
||||
idx = np.searchsorted(pos_x_array, path_height, side='right')
|
||||
return int(np.clip(idx - 1, 0, len(pos_x_array) - 1))
|
||||
if len(pos_x_array) == 0:
|
||||
return 0
|
||||
indices = np.where(pos_x_array <= path_height)[0]
|
||||
return indices[-1] if indices.size > 0 else 0
|
||||
|
||||
def _map_to_screen(self, in_x, in_y, in_z):
|
||||
"""Project a point in car space to screen space"""
|
||||
@@ -334,69 +349,82 @@ class ModelRenderer:
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
|
||||
# Slice points and filter non-negative x-coordinates
|
||||
points = line[:max_idx + 1][line[:max_idx + 1, 0] >= 0]
|
||||
points = line[:max_idx + 1]
|
||||
points = points[points[:, 0] >= 0]
|
||||
if points.shape[0] == 0:
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
|
||||
# Create left and right 3D points in one array
|
||||
n_points = points.shape[0]
|
||||
points_3d = np.empty((n_points * 2, 3), dtype=np.float32)
|
||||
points_3d[:n_points, 0] = points_3d[n_points:, 0] = points[:, 0]
|
||||
points_3d[:n_points, 1] = points[:, 1] - y_off
|
||||
points_3d[n_points:, 1] = points[:, 1] + y_off
|
||||
points_3d[:n_points, 2] = points_3d[n_points:, 2] = points[:, 2] + z_off
|
||||
N = points.shape[0]
|
||||
# Generate left and right 3D points in one array using broadcasting
|
||||
offsets = np.array([[0, -y_off, z_off], [0, y_off, z_off]], dtype=np.float32)
|
||||
points_3d = points[None, :, :] + offsets[:, None, :] # Shape: 2xNx3
|
||||
points_3d = points_3d.reshape(2 * N, 3) # Shape: (2*N)x3
|
||||
|
||||
# Single matrix multiplication for projections
|
||||
proj = self._car_space_transform @ points_3d.T
|
||||
valid_z = np.abs(proj[2]) > 1e-6
|
||||
if not np.any(valid_z):
|
||||
# Transform all points to projected space in one operation
|
||||
proj = self._car_space_transform @ points_3d.T # Shape: 3x(2*N)
|
||||
proj = proj.reshape(3, 2, N)
|
||||
left_proj = proj[:, 0, :]
|
||||
right_proj = proj[:, 1, :]
|
||||
|
||||
# Filter points where z is sufficiently large
|
||||
valid_proj = (np.abs(left_proj[2]) >= 1e-6) & (np.abs(right_proj[2]) >= 1e-6)
|
||||
if not np.any(valid_proj):
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
|
||||
# Compute screen coordinates
|
||||
screen = proj[:2, valid_z] / proj[2, valid_z][None, :]
|
||||
left_screen = screen[:, :n_points].T
|
||||
right_screen = screen[:, n_points:].T
|
||||
left_screen = left_proj[:2, valid_proj] / left_proj[2, valid_proj][None, :]
|
||||
right_screen = right_proj[:2, valid_proj] / right_proj[2, valid_proj][None, :]
|
||||
|
||||
# Ensure consistent shapes by re-aligning valid points
|
||||
valid_points = np.minimum(left_screen.shape[0], right_screen.shape[0])
|
||||
if valid_points == 0:
|
||||
# Define clip region bounds
|
||||
clip = self._clip_region
|
||||
x_min, x_max = clip.x, clip.x + clip.width
|
||||
y_min, y_max = clip.y, clip.y + clip.height
|
||||
|
||||
# Filter points within clip region
|
||||
left_in_clip = (
|
||||
(left_screen[0] >= x_min) & (left_screen[0] <= x_max) &
|
||||
(left_screen[1] >= y_min) & (left_screen[1] <= y_max)
|
||||
)
|
||||
right_in_clip = (
|
||||
(right_screen[0] >= x_min) & (right_screen[0] <= x_max) &
|
||||
(right_screen[1] >= y_min) & (right_screen[1] <= y_max)
|
||||
)
|
||||
both_in_clip = left_in_clip & right_in_clip
|
||||
|
||||
if not np.any(both_in_clip):
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
left_screen = left_screen[:valid_points]
|
||||
right_screen = right_screen[:valid_points]
|
||||
|
||||
if self._clip_region:
|
||||
clip = self._clip_region
|
||||
bounds_mask = (
|
||||
(left_screen[:, 0] >= clip.x) & (left_screen[:, 0] <= clip.x + clip.width) &
|
||||
(left_screen[:, 1] >= clip.y) & (left_screen[:, 1] <= clip.y + clip.height) &
|
||||
(right_screen[:, 0] >= clip.x) & (right_screen[:, 0] <= clip.x + clip.width) &
|
||||
(right_screen[:, 1] >= clip.y) & (right_screen[:, 1] <= clip.y + clip.height)
|
||||
)
|
||||
if not np.any(bounds_mask):
|
||||
# Select valid and clipped points
|
||||
left_screen = left_screen[:, both_in_clip]
|
||||
right_screen = right_screen[:, both_in_clip]
|
||||
|
||||
# Handle Y-coordinate inversion on hills
|
||||
if not allow_invert and left_screen.shape[1] > 1:
|
||||
y = left_screen[1, :] # y-coordinates
|
||||
keep = y == np.minimum.accumulate(y)
|
||||
if not np.any(keep):
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
left_screen = left_screen[bounds_mask]
|
||||
right_screen = right_screen[bounds_mask]
|
||||
left_screen = left_screen[:, keep]
|
||||
right_screen = right_screen[:, keep]
|
||||
|
||||
if not allow_invert and left_screen.shape[0] > 1:
|
||||
keep = np.concatenate(([True], np.diff(left_screen[:, 1]) < 0))
|
||||
left_screen = left_screen[keep]
|
||||
right_screen = right_screen[keep]
|
||||
if left_screen.shape[0] == 0:
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
|
||||
return np.vstack((left_screen, right_screen[::-1])).astype(np.float32)
|
||||
return np.vstack((left_screen.T, right_screen[:, ::-1].T)).astype(np.float32)
|
||||
|
||||
@staticmethod
|
||||
def _map_val(x, x0, x1, y0, y1):
|
||||
x = max(x0, min(x, x1))
|
||||
x = np.clip(x, x0, x1)
|
||||
ra = x1 - x0
|
||||
rb = y1 - y0
|
||||
return (x - x0) * rb / ra + y0 if ra != 0 else y0
|
||||
|
||||
@staticmethod
|
||||
def _hsla_to_color(h, s, l, a):
|
||||
r, g, b = [max(0, min(255, int(v * 255))) for v in colorsys.hls_to_rgb(h, l, s)]
|
||||
return rl.Color(r, g, b, max(0, min(255, int(a * 255))))
|
||||
rgb = colorsys.hls_to_rgb(h, l, s)
|
||||
return rl.Color(
|
||||
int(rgb[0] * 255),
|
||||
int(rgb[1] * 255),
|
||||
int(rgb[2] * 255),
|
||||
int(a * 255)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _blend_colors(begin_colors, end_colors, t):
|
||||
@@ -79,7 +79,8 @@ Networking::Networking(QWidget* parent, bool show_advanced) : QFrame(parent) {
|
||||
}
|
||||
|
||||
void Networking::setPrimeType(PrimeState::Type type) {
|
||||
an->setGsmVisible(type == PrimeState::PRIME_TYPE_NONE || type == PrimeState::PRIME_TYPE_LITE);
|
||||
an->setGsmVisible(type == PrimeState::PRIME_TYPE_NONE || type == PrimeState::PRIME_TYPE_UNKNOWN || \
|
||||
type == PrimeState::PRIME_TYPE_PURPLE || type == PrimeState::PRIME_TYPE_LITE);
|
||||
wifi->ipv4_forward = (type == PrimeState::PRIME_TYPE_NONE || type == PrimeState::PRIME_TYPE_LITE);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ FirehosePanel::FirehosePanel(SettingsWindow *parent) : QWidget((QWidget*)parent)
|
||||
layout->setSpacing(20);
|
||||
|
||||
// header
|
||||
QLabel *title = new QLabel(tr("🔥 Firehose Mode 🔥"));
|
||||
QLabel *title = new QLabel(tr("Firehose Mode"));
|
||||
title->setStyleSheet("font-size: 100px; font-weight: 500; font-family: 'Noto Color Emoji';");
|
||||
layout->addWidget(title, 0, Qt::AlignCenter);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) {
|
||||
main_layout->setContentsMargins(40, 40, 40, 40);
|
||||
|
||||
// top header
|
||||
QHBoxLayout* header_layout = new QHBoxLayout();
|
||||
header_layout = new QHBoxLayout();
|
||||
header_layout->setContentsMargins(0, 0, 0, 0);
|
||||
header_layout->setSpacing(16);
|
||||
|
||||
|
||||
@@ -39,11 +39,13 @@ public:
|
||||
|
||||
protected:
|
||||
QHBoxLayout *home_layout;
|
||||
QHBoxLayout *header_layout;
|
||||
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void refresh();
|
||||
|
||||
private:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void hideEvent(QHideEvent *event) override;
|
||||
void refresh();
|
||||
|
||||
Params params;
|
||||
|
||||
|
||||
@@ -139,6 +139,15 @@ void TogglesPanel::expandToggleDescription(const QString ¶m) {
|
||||
toggles[param.toStdString()]->showDescription();
|
||||
}
|
||||
|
||||
void TogglesPanel::scrollToToggle(const QString ¶m) {
|
||||
if (auto it = toggles.find(param.toStdString()); it != toggles.end()) {
|
||||
auto scroll_area = qobject_cast<QScrollArea*>(parent()->parent());
|
||||
if (scroll_area) {
|
||||
scroll_area->ensureWidgetVisible(it->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TogglesPanel::showEvent(QShowEvent *event) {
|
||||
updateToggles();
|
||||
}
|
||||
@@ -222,7 +231,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
addItem(dcamBtn);
|
||||
#endif
|
||||
|
||||
auto resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), "");
|
||||
resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), "");
|
||||
connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription);
|
||||
connect(resetCalibBtn, &ButtonControl::clicked, [&]() {
|
||||
if (!uiState()->engaged()) {
|
||||
@@ -235,6 +244,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
params.remove("LiveParametersV2");
|
||||
params.remove("LiveDelay");
|
||||
params.putBool("OnroadCycleRequested", true);
|
||||
updateCalibDescription();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -313,9 +323,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
}
|
||||
|
||||
void DevicePanel::updateCalibDescription() {
|
||||
QString desc =
|
||||
tr("sunnypilot requires the device to be mounted within 4° left or right and "
|
||||
"within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required.");
|
||||
QString desc = tr("sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down.");
|
||||
std::string calib_bytes = params.get("CalibrationParams");
|
||||
if (!calib_bytes.empty()) {
|
||||
try {
|
||||
@@ -333,8 +341,53 @@ void DevicePanel::updateCalibDescription() {
|
||||
qInfo() << "invalid CalibrationParams";
|
||||
}
|
||||
}
|
||||
desc += tr(" Resetting calibration will restart openpilot if the car is powered on.");
|
||||
qobject_cast<ButtonControl *>(sender())->setDescription(desc);
|
||||
|
||||
const bool is_release = params.getBool("IsReleaseBranch");
|
||||
if (!is_release) {
|
||||
int lag_perc = 0;
|
||||
std::string lag_bytes = params.get("LiveDelay");
|
||||
if (!lag_bytes.empty()) {
|
||||
try {
|
||||
AlignedBuffer aligned_buf;
|
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(lag_bytes.data(), lag_bytes.size()));
|
||||
lag_perc = cmsg.getRoot<cereal::Event>().getLiveDelay().getCalPerc();
|
||||
} catch (kj::Exception) {
|
||||
qInfo() << "invalid LiveDelay";
|
||||
}
|
||||
}
|
||||
desc += "\n\n";
|
||||
if (lag_perc < 100) {
|
||||
desc += tr("Steering lag calibration is %1% complete.").arg(lag_perc);
|
||||
} else {
|
||||
desc += tr("Steering lag calibration is complete.");
|
||||
}
|
||||
}
|
||||
|
||||
std::string torque_bytes = params.get("LiveTorqueParameters");
|
||||
if (!torque_bytes.empty()) {
|
||||
try {
|
||||
AlignedBuffer aligned_buf;
|
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(torque_bytes.data(), torque_bytes.size()));
|
||||
auto torque = cmsg.getRoot<cereal::Event>().getLiveTorqueParameters();
|
||||
// don't add for non-torque cars
|
||||
if (torque.getUseParams()) {
|
||||
int torque_perc = torque.getCalPerc();
|
||||
desc += is_release ? "\n\n" : " ";
|
||||
if (torque_perc < 100) {
|
||||
desc += tr("Steering torque response calibration is %1% complete.").arg(torque_perc);
|
||||
} else {
|
||||
desc += tr("Steering torque response calibration is complete.");
|
||||
}
|
||||
}
|
||||
} catch (kj::Exception) {
|
||||
qInfo() << "invalid LiveTorqueParameters";
|
||||
}
|
||||
}
|
||||
|
||||
desc += "\n\n";
|
||||
desc += tr("openpilot is continuously calibrating, resetting is rarely required. "
|
||||
"Resetting calibration will restart openpilot if the car is powered on.");
|
||||
resetCalibBtn->setDescription(desc);
|
||||
}
|
||||
|
||||
void DevicePanel::reboot() {
|
||||
@@ -387,6 +440,7 @@ void SettingsWindow::setCurrentPanel(int index, const QString ¶m) {
|
||||
}
|
||||
} else {
|
||||
emit expandToggleDescription(param);
|
||||
emit scrollToToggle(param);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,6 +481,7 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) {
|
||||
|
||||
TogglesPanel *toggles = new TogglesPanel(this);
|
||||
QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription);
|
||||
QObject::connect(this, &SettingsWindow::scrollToToggle, toggles, &TogglesPanel::scrollToToggle);
|
||||
|
||||
auto networking = new Networking(this);
|
||||
QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType);
|
||||
|
||||
@@ -42,6 +42,7 @@ signals:
|
||||
void reviewTrainingGuide();
|
||||
void showDriverView();
|
||||
void expandToggleDescription(const QString ¶m);
|
||||
void scrollToToggle(const QString ¶m);
|
||||
|
||||
protected:
|
||||
QPushButton *sidebar_alert_widget;
|
||||
@@ -67,6 +68,7 @@ protected slots:
|
||||
protected:
|
||||
Params params;
|
||||
ButtonControl *pair_device;
|
||||
ButtonControl *resetCalibBtn;
|
||||
};
|
||||
|
||||
class TogglesPanel : public ListWidget {
|
||||
@@ -77,6 +79,7 @@ public:
|
||||
|
||||
public slots:
|
||||
void expandToggleDescription(const QString ¶m);
|
||||
void scrollToToggle(const QString ¶m);
|
||||
|
||||
protected slots:
|
||||
virtual void updateState(const UIState &s);
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
#ifdef SUNNYPILOT
|
||||
#include "selfdrive/ui/sunnypilot/qt/onroad/buttons.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/onroad/hud.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/onroad/model.h"
|
||||
#define ExperimentalButton ExperimentalButtonSP
|
||||
#define ModelRenderer ModelRendererSP
|
||||
#else
|
||||
#include "selfdrive/ui/qt/onroad/buttons.h"
|
||||
#include "selfdrive/ui/qt/onroad/hud.h"
|
||||
|
||||
@@ -1,18 +1,5 @@
|
||||
#include "selfdrive/ui/qt/onroad/model.h"
|
||||
|
||||
constexpr int CLIP_MARGIN = 500;
|
||||
constexpr float MIN_DRAW_DISTANCE = 10.0;
|
||||
constexpr float MAX_DRAW_DISTANCE = 100.0;
|
||||
|
||||
static int get_path_length_idx(const cereal::XYZTData::Reader &line, const float path_height) {
|
||||
const auto &line_x = line.getX();
|
||||
int max_idx = 0;
|
||||
for (int i = 1; i < line_x.size() && line_x[i] <= path_height; ++i) {
|
||||
max_idx = i;
|
||||
}
|
||||
return max_idx;
|
||||
}
|
||||
|
||||
void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) {
|
||||
auto *s = uiState();
|
||||
auto &sm = *(s->sm);
|
||||
@@ -35,7 +22,7 @@ void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) {
|
||||
|
||||
update_model(model, lead_one);
|
||||
drawLaneLines(painter);
|
||||
drawPath(painter, model, surface_rect.height());
|
||||
drawPath(painter, model, surface_rect);
|
||||
|
||||
if (longitudinal_control && sm.alive("radarState")) {
|
||||
update_leads(radar_state, model.getPosition());
|
||||
|
||||
@@ -9,21 +9,39 @@
|
||||
#include "selfdrive/ui/ui.h"
|
||||
#endif
|
||||
|
||||
constexpr int CLIP_MARGIN = 500;
|
||||
constexpr float MIN_DRAW_DISTANCE = 10.0;
|
||||
constexpr float MAX_DRAW_DISTANCE = 100.0;
|
||||
|
||||
inline int get_path_length_idx(const cereal::XYZTData::Reader &line, const float path_height) {
|
||||
const auto &line_x = line.getX();
|
||||
int max_idx = 0;
|
||||
for (int i = 1; i < line_x.size() && line_x[i] <= path_height; ++i) {
|
||||
max_idx = i;
|
||||
}
|
||||
return max_idx;
|
||||
}
|
||||
|
||||
class ModelRenderer {
|
||||
public:
|
||||
virtual ~ModelRenderer() = default;
|
||||
|
||||
ModelRenderer() {}
|
||||
void setTransform(const Eigen::Matrix3f &transform) { car_space_transform = transform; }
|
||||
void draw(QPainter &painter, const QRect &surface_rect);
|
||||
|
||||
private:
|
||||
protected:
|
||||
bool mapToScreen(float in_x, float in_y, float in_z, QPointF *out);
|
||||
void mapLineToPolygon(const cereal::XYZTData::Reader &line, float y_off, float z_off,
|
||||
QPolygonF *pvd, int max_idx, bool allow_invert = true);
|
||||
void drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, const QPointF &vd, const QRect &surface_rect);
|
||||
void update_leads(const cereal::RadarState::Reader &radar_state, const cereal::XYZTData::Reader &line);
|
||||
void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead);
|
||||
virtual void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead);
|
||||
void drawLaneLines(QPainter &painter);
|
||||
void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, int height);
|
||||
virtual void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, const QRect &surface_rect) {;
|
||||
drawPath(painter, model, surface_rect.height());
|
||||
}
|
||||
void updatePathGradient(QLinearGradient &bg);
|
||||
QColor blendColors(const QColor &start, const QColor &end, float t);
|
||||
|
||||
|
||||
@@ -136,6 +136,59 @@ QWidget * Setup::low_voltage() {
|
||||
return widget;
|
||||
}
|
||||
|
||||
QWidget * Setup::custom_software_warning() {
|
||||
QWidget *widget = new QWidget();
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(widget);
|
||||
main_layout->setContentsMargins(55, 0, 55, 55);
|
||||
main_layout->setSpacing(0);
|
||||
|
||||
QVBoxLayout *inner_layout = new QVBoxLayout();
|
||||
inner_layout->setContentsMargins(110, 110, 300, 0);
|
||||
main_layout->addLayout(inner_layout);
|
||||
|
||||
QLabel *title = new QLabel(tr("WARNING: Custom Software"));
|
||||
title->setStyleSheet("font-size: 90px; font-weight: 500; color: #FF594F;");
|
||||
inner_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft);
|
||||
|
||||
inner_layout->addSpacing(25);
|
||||
|
||||
QLabel *body = new QLabel(tr("Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.\n\nIf you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later."));
|
||||
body->setWordWrap(true);
|
||||
body->setAlignment(Qt::AlignTop | Qt::AlignLeft);
|
||||
body->setStyleSheet("font-size: 65px; font-weight: 300;");
|
||||
inner_layout->addWidget(body);
|
||||
|
||||
inner_layout->addStretch();
|
||||
|
||||
QHBoxLayout *blayout = new QHBoxLayout();
|
||||
blayout->setSpacing(50);
|
||||
main_layout->addLayout(blayout, 0);
|
||||
|
||||
QPushButton *back = new QPushButton(tr("Back"));
|
||||
back->setObjectName("navBtn");
|
||||
blayout->addWidget(back);
|
||||
QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage);
|
||||
|
||||
QPushButton *cont = new QPushButton(tr("Continue"));
|
||||
cont->setObjectName("navBtn");
|
||||
blayout->addWidget(cont);
|
||||
QObject::connect(cont, &QPushButton::clicked, this, [=]() {
|
||||
QTimer::singleShot(0, [=]() {
|
||||
setCurrentWidget(downloading_widget);
|
||||
});
|
||||
QString url = InputDialog::getText(tr("Enter URL"), this, tr("for Custom Software"));
|
||||
if (!url.isEmpty()) {
|
||||
QTimer::singleShot(1000, this, [=]() {
|
||||
download(url);
|
||||
});
|
||||
} else {
|
||||
setCurrentWidget(software_selection_widget);
|
||||
}
|
||||
});
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
QWidget * Setup::getting_started() {
|
||||
QWidget *widget = new QWidget();
|
||||
|
||||
@@ -305,20 +358,17 @@ QWidget * Setup::software_selection() {
|
||||
blayout->addWidget(cont);
|
||||
|
||||
QObject::connect(cont, &QPushButton::clicked, [=]() {
|
||||
auto w = currentWidget();
|
||||
QTimer::singleShot(0, [=]() {
|
||||
setCurrentWidget(downloading_widget);
|
||||
});
|
||||
QString url = OPENPILOT_URL;
|
||||
if (group->checkedButton() != openpilot) {
|
||||
url = InputDialog::getText(tr("Enter URL"), this, tr("for Custom Software"));
|
||||
}
|
||||
if (!url.isEmpty()) {
|
||||
QTimer::singleShot(1000, this, [=]() {
|
||||
download(url);
|
||||
QTimer::singleShot(0, [=]() {
|
||||
setCurrentWidget(custom_software_warning_widget);
|
||||
});
|
||||
} else {
|
||||
setCurrentWidget(w);
|
||||
QTimer::singleShot(0, [=]() {
|
||||
setCurrentWidget(downloading_widget);
|
||||
});
|
||||
QTimer::singleShot(1000, this, [=]() {
|
||||
download(OPENPILOT_URL);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -415,8 +465,10 @@ Setup::Setup(QWidget *parent) : QStackedWidget(parent) {
|
||||
|
||||
addWidget(getting_started());
|
||||
addWidget(network_setup());
|
||||
addWidget(software_selection());
|
||||
|
||||
software_selection_widget = software_selection();
|
||||
addWidget(software_selection_widget);
|
||||
custom_software_warning_widget = custom_software_warning();
|
||||
addWidget(custom_software_warning_widget);
|
||||
downloading_widget = downloading();
|
||||
addWidget(downloading_widget);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ public:
|
||||
private:
|
||||
void selectLanguage();
|
||||
QWidget *low_voltage();
|
||||
QWidget *custom_software_warning();
|
||||
QWidget *getting_started();
|
||||
QWidget *network_setup();
|
||||
QWidget *software_selection();
|
||||
@@ -23,6 +24,8 @@ private:
|
||||
|
||||
QWidget *failed_widget;
|
||||
QWidget *downloading_widget;
|
||||
QWidget *custom_software_warning_widget;
|
||||
QWidget *software_selection_widget;
|
||||
QTranslator translator;
|
||||
|
||||
signals:
|
||||
|
||||
@@ -19,18 +19,22 @@ qt_src = [
|
||||
"sunnypilot/qt/sidebar.cc",
|
||||
"sunnypilot/qt/window.cc",
|
||||
"sunnypilot/qt/home.cc",
|
||||
"sunnypilot/qt/offroad/exit_offroad_button.cc",
|
||||
"sunnypilot/qt/offroad/offroad_home.cc",
|
||||
"sunnypilot/qt/offroad/settings/device_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/lateral_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/longitudinal_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/max_time_offroad.cc",
|
||||
"sunnypilot/qt/offroad/settings/brightness.cc",
|
||||
"sunnypilot/qt/offroad/settings/models_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/osm_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/settings.cc",
|
||||
"sunnypilot/qt/offroad/settings/software_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/sunnylink_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc",
|
||||
"sunnypilot/qt/offroad/settings/trips_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/vehicle_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/visuals_panel.cc",
|
||||
"sunnypilot/qt/onroad/annotated_camera.cc",
|
||||
"sunnypilot/qt/onroad/buttons.cc",
|
||||
"sunnypilot/qt/onroad/hud.cc",
|
||||
@@ -39,11 +43,16 @@ qt_src = [
|
||||
]
|
||||
|
||||
lateral_panel_qt_src = [
|
||||
"sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.cc",
|
||||
"sunnypilot/qt/offroad/settings/lateral/lane_change_settings.cc",
|
||||
"sunnypilot/qt/offroad/settings/lateral/mads_settings.cc",
|
||||
"sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc",
|
||||
]
|
||||
|
||||
longitudinal_panel_qt_src = [
|
||||
"sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.cc",
|
||||
]
|
||||
|
||||
network_src = [
|
||||
"sunnypilot/qt/network/sunnylink/sunnylink_client.cc",
|
||||
"sunnypilot/qt/network/sunnylink/services/base_device_service.cc",
|
||||
@@ -51,6 +60,10 @@ network_src = [
|
||||
"sunnypilot/qt/network/sunnylink/services/user_service.cc",
|
||||
]
|
||||
|
||||
osm_panel_qt_src = [
|
||||
"sunnypilot/qt/offroad/settings/osm/models_fetcher.cc",
|
||||
]
|
||||
|
||||
vehicle_panel_qt_src = [
|
||||
"sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.cc",
|
||||
"sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.cc",
|
||||
@@ -72,8 +85,9 @@ brand_settings_qt_src = [
|
||||
"sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.cc",
|
||||
]
|
||||
|
||||
|
||||
sp_widgets_src = widgets_src + network_src
|
||||
sp_qt_src = qt_src + lateral_panel_qt_src + vehicle_panel_qt_src + brand_settings_qt_src
|
||||
sp_qt_src = qt_src + lateral_panel_qt_src + vehicle_panel_qt_src + brand_settings_qt_src + longitudinal_panel_qt_src + osm_panel_qt_src
|
||||
sp_qt_util = qt_util
|
||||
|
||||
Export('sp_widgets_src', 'sp_qt_src', "sp_qt_util")
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QEventLoop>
|
||||
|
||||
class JsonFetcher {
|
||||
public:
|
||||
static QJsonObject getJsonFromURL(const QString &url) {
|
||||
const auto qurl = QUrl(url);
|
||||
QNetworkAccessManager manager;
|
||||
const QNetworkRequest request(qurl);
|
||||
QNetworkReply *reply = manager.get(request);
|
||||
QEventLoop loop;
|
||||
|
||||
// Send GET request
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "Failed to fetch data from URL: " << reply->errorString();
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
const QByteArray responseData = reply->readAll();
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(responseData);
|
||||
QJsonObject json = doc.object();
|
||||
|
||||
reply->deleteLater();
|
||||
return json;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
#include <QDebug>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QStyle>
|
||||
|
||||
#include "selfdrive/ui/ui.h"
|
||||
#include "selfdrive/ui/qt/widgets/input.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.h"
|
||||
|
||||
|
||||
ExitOffroadButton::ExitOffroadButton(QWidget *parent) : QPushButton(parent), glowTimer(new QTimer(this)) {
|
||||
setMouseTracking(true);
|
||||
|
||||
connect(glowTimer, &QTimer::timeout, this, [this]() {
|
||||
// Pulse alpha up and down
|
||||
glowAlpha += glowDelta;
|
||||
if (glowAlpha > 220 || glowAlpha < 10) {
|
||||
glowDelta *= -1;
|
||||
}
|
||||
update(); // trigger repaint
|
||||
});
|
||||
|
||||
glowTimer->start(45);
|
||||
|
||||
pixmap = QPixmap("../../sunnypilot/selfdrive/assets/offroad/icon_exit_offroad.png").scaledToWidth(img_width, Qt::SmoothTransformation);
|
||||
|
||||
// go to toggles and expand experimental mode description
|
||||
connect(this, &QPushButton::clicked, [=]() {
|
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to exit Always Offroad mode?"), tr("Confirm"), this)) {
|
||||
params.remove("OffroadMode");
|
||||
}
|
||||
});
|
||||
|
||||
setFixedHeight(125);
|
||||
QHBoxLayout *main_layout = new QHBoxLayout;
|
||||
main_layout->setContentsMargins(horizontal_padding, 0, horizontal_padding, 0);
|
||||
|
||||
mode_label = new QLabel(tr("EXIT ALWAYS OFFROAD MODE"));
|
||||
mode_icon = new QLabel;
|
||||
mode_icon->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
|
||||
mode_icon->setPixmap(pixmap);
|
||||
|
||||
main_layout->addWidget(mode_label, 1, Qt::AlignLeft);
|
||||
main_layout->addWidget(mode_icon, 0, Qt::AlignRight);
|
||||
|
||||
setLayout(main_layout);
|
||||
|
||||
setStyleSheet(R"(
|
||||
QPushButton {
|
||||
border: none;
|
||||
}
|
||||
|
||||
QLabel {
|
||||
font-size: 45px;
|
||||
font-weight: 300;
|
||||
text-align: left;
|
||||
font-family: JetBrainsMono;
|
||||
color: #000000;
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
void drawPulsingGlowOverlay(QPainter &p, QPainterPath path, int glowAlpha) {
|
||||
// Draw pulsing glow effect clipped to button area
|
||||
p.save();
|
||||
p.setClipPath(path);
|
||||
p.setCompositionMode(QPainter::CompositionMode_HardLight);
|
||||
|
||||
const QColor animatedGlowColor(255, 255, 255, std::min(255, glowAlpha));
|
||||
QPen glowPen(animatedGlowColor, 8);
|
||||
glowPen.setJoinStyle(Qt::RoundJoin);
|
||||
p.setPen(glowPen);
|
||||
p.drawPath(path);
|
||||
|
||||
p.restore();
|
||||
}
|
||||
|
||||
void ExitOffroadButton::paintEvent(QPaintEvent *event) {
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
QPainterPath path;
|
||||
path.addRoundedRect(rect(), 10, 10);
|
||||
|
||||
// gradient
|
||||
bool pressed = isDown();
|
||||
QLinearGradient gradient(rect().left(), 0, rect().right(), 0);
|
||||
gradient.setColorAt(0, QColor(35, 149, 255, pressed ? 0xcc : 0xff));
|
||||
gradient.setColorAt(0.3, QColor(35, 149, 255, pressed ? 0xcc : 0xff));
|
||||
gradient.setColorAt(1, QColor(20, 255, 171, pressed ? 0xcc : 0xff));
|
||||
p.fillPath(path, gradient);
|
||||
|
||||
drawPulsingGlowOverlay(p, path, glowAlpha);
|
||||
|
||||
// vertical line
|
||||
p.setPen(QPen(QColor(0, 0, 0, 0x4d), 3, Qt::SolidLine));
|
||||
int line_x = rect().right() - img_width - (2 * horizontal_padding);
|
||||
p.drawLine(line_x, rect().bottom(), line_x, rect().top());
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
|
||||
#include "common/params.h"
|
||||
|
||||
class ExitOffroadButton : public QPushButton {
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
QTimer *glowTimer;
|
||||
int glowAlpha = 100; // Current alpha of glow
|
||||
int glowDelta = 10; // Change per tick
|
||||
|
||||
public:
|
||||
explicit ExitOffroadButton(QWidget* parent = 0);
|
||||
|
||||
Params params;
|
||||
bool offroad_mode;
|
||||
int img_width = 100;
|
||||
int horizontal_padding = 30;
|
||||
QPixmap pixmap;
|
||||
QLabel *mode_label;
|
||||
QLabel *mode_icon;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
};
|
||||
@@ -5,16 +5,45 @@
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h"
|
||||
|
||||
#include <QStackedWidget>
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/drive_stats.h"
|
||||
|
||||
OffroadHomeSP::OffroadHomeSP(QWidget *parent) : OffroadHome(parent) {
|
||||
QStackedWidget *left_widget = new QStackedWidget(this);
|
||||
left_widget->addWidget(new DriveStats(this));
|
||||
QFrame *left_widget = new QFrame(this);
|
||||
QVBoxLayout *left_layout = new QVBoxLayout(left_widget);
|
||||
left_layout->setContentsMargins(0, 0, 0, 0);
|
||||
left_layout->setSpacing(30);
|
||||
|
||||
btn_exit_offroad = new ExitOffroadButton(this);
|
||||
QObject::connect(btn_exit_offroad, &ExitOffroadButton::clicked, [=]() {
|
||||
refreshOffroadStatus();
|
||||
});
|
||||
left_layout->addWidget(btn_exit_offroad);
|
||||
|
||||
left_layout->addWidget(new DriveStats(this));
|
||||
left_widget->setStyleSheet("border-radius: 10px;");
|
||||
|
||||
home_layout->insertWidget(0, left_widget);
|
||||
|
||||
offroad_notif = new QPushButton(tr("ALWAYS OFFROAD ACTIVE"));
|
||||
offroad_notif->setVisible(false);
|
||||
offroad_notif->setStyleSheet("background-color: #E22C2C;");
|
||||
header_layout->insertWidget(0, offroad_notif, 0, Qt::AlignHCenter | Qt::AlignLeft);
|
||||
|
||||
QObject::connect(deviceSP(), &DeviceSP::displayPowerChanged, this, &OffroadHomeSP::refreshOffroadStatus);
|
||||
|
||||
}
|
||||
|
||||
void OffroadHomeSP::showEvent(QShowEvent *event) {
|
||||
refreshOffroadStatus();
|
||||
OffroadHome::showEvent(event);
|
||||
}
|
||||
|
||||
void OffroadHomeSP::refreshOffroadStatus() {
|
||||
bool is_offroad = params.getBool("OffroadMode");
|
||||
btn_exit_offroad->setVisible(is_offroad);
|
||||
offroad_notif->setVisible(is_offroad);
|
||||
OffroadHome::refresh();
|
||||
}
|
||||
|
||||
@@ -8,10 +8,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/qt/offroad/offroad_home.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.h"
|
||||
|
||||
class OffroadHomeSP : public OffroadHome {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit OffroadHomeSP(QWidget *parent = 0);
|
||||
|
||||
private:
|
||||
ExitOffroadButton *btn_exit_offroad;
|
||||
QPushButton *offroad_notif;
|
||||
Params params;
|
||||
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void refreshOffroadStatus();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.h"
|
||||
|
||||
// Map of Brightness Options
|
||||
const QMap<QString, QString> Brightness::brightness_options = {
|
||||
{"0", "1"}, // Auto (Dark)
|
||||
{"1", "0"}, // Auto
|
||||
{"2", "10"},
|
||||
{"3", "20"},
|
||||
{"4", "30"},
|
||||
{"5", "40"},
|
||||
{"6", "50"},
|
||||
{"7", "60"},
|
||||
{"8", "70"},
|
||||
{"9", "80"},
|
||||
{"10", "90"},
|
||||
{"11", "100"}
|
||||
};
|
||||
|
||||
Brightness::Brightness() : OptionControlSP(
|
||||
"Brightness",
|
||||
tr("Brightness"),
|
||||
tr("Overrides the brightness of the device."),
|
||||
"../assets/offroad/icon_blank.png",
|
||||
{0, 11}, 1, true, &brightness_options) {
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void Brightness::refresh() {
|
||||
const int brightness = QString::fromStdString(params.get("Brightness")).toInt();
|
||||
|
||||
QString label;
|
||||
if (brightness == 1) {
|
||||
label = tr("Auto (Dark)");
|
||||
} else if (brightness == 0) {
|
||||
label = tr("Auto");
|
||||
} else {
|
||||
const int value = brightness;
|
||||
label = QString("%1").arg(value);
|
||||
}
|
||||
|
||||
setLabel(label);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
|
||||
class Brightness : public OptionControlSP {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static const QMap<QString, QString> brightness_options;
|
||||
|
||||
Brightness();
|
||||
void refresh();
|
||||
|
||||
private:
|
||||
Params params;
|
||||
};
|
||||
@@ -80,6 +80,19 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) {
|
||||
connect(maxTimeOffroad, &OptionControlSP::updateLabels, maxTimeOffroad, &MaxTimeOffroad::refresh);
|
||||
addItem(maxTimeOffroad);
|
||||
|
||||
toggleDeviceBootMode = new ButtonParamControlSP("DeviceBootMode", tr("Wake-Up Behavior"), "", "", {"Default", "Offroad"}, 375, true);
|
||||
addItem(toggleDeviceBootMode);
|
||||
|
||||
connect(toggleDeviceBootMode, &ButtonParamControlSP::buttonClicked, this, [=](int index) {
|
||||
params.put("DeviceBootMode", QString::number(index).toStdString());
|
||||
updateState();
|
||||
});
|
||||
|
||||
// Brightness
|
||||
brightness = new Brightness();
|
||||
connect(brightness, &OptionControlSP::updateLabels, brightness, &Brightness::refresh);
|
||||
addItem(brightness);
|
||||
|
||||
addItem(device_grid_layout);
|
||||
|
||||
// offroad mode and power buttons
|
||||
@@ -179,4 +192,10 @@ void DevicePanelSP::updateState() {
|
||||
bool offroad_mode_param = params.getBool("OffroadMode");
|
||||
offroadBtn->setText(offroad_mode_param ? tr("Exit Always Offroad") : tr("Always Offroad"));
|
||||
offroadBtn->setStyleSheet(offroad_mode_param ? alwaysOffroadStyle : autoOffroadStyle);
|
||||
|
||||
DeviceSleepModeStatus currStatus = DeviceSleepModeStatus::DEFAULT;
|
||||
if (params.get("DeviceBootMode") == "1") {
|
||||
currStatus = DeviceSleepModeStatus::OFFROAD;
|
||||
}
|
||||
toggleDeviceBootMode->setDescription(deviceSleepModeDescription(currStatus));
|
||||
}
|
||||
|
||||
@@ -8,9 +8,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
|
||||
enum class DeviceSleepModeStatus {
|
||||
DEFAULT,
|
||||
OFFROAD,
|
||||
};
|
||||
|
||||
class DevicePanelSP : public DevicePanel {
|
||||
Q_OBJECT
|
||||
|
||||
@@ -25,6 +31,8 @@ private:
|
||||
std::map<QString, PushButtonSP*> buttons;
|
||||
PushButtonSP *offroadBtn;
|
||||
MaxTimeOffroad *maxTimeOffroad;
|
||||
ButtonParamControlSP *toggleDeviceBootMode;
|
||||
Brightness *brightness;
|
||||
|
||||
const QString alwaysOffroadStyle = R"(
|
||||
PushButtonSP {
|
||||
@@ -85,4 +93,20 @@ private:
|
||||
background-color: #FF2424;
|
||||
}
|
||||
)";
|
||||
|
||||
static QString deviceSleepModeDescription(DeviceSleepModeStatus status = DeviceSleepModeStatus::DEFAULT) {
|
||||
QString def_str = tr("⁍ Default: Device will boot/wake-up normally & will be ready to engage.");
|
||||
QString offrd_str = tr("⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up.");
|
||||
|
||||
if (status == DeviceSleepModeStatus::DEFAULT) {
|
||||
def_str = "<font color='white'><b>" + def_str + "</b></font>";
|
||||
} else if (status == DeviceSleepModeStatus::OFFROAD) {
|
||||
offrd_str = "<font color='white'><b>" + offrd_str + "</b></font>";
|
||||
}
|
||||
|
||||
return QString("%1<br><br>%2<br>%3")
|
||||
.arg(tr("Controls state of the device after boot/sleep."))
|
||||
.arg(def_str)
|
||||
.arg(offrd_str);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.h"
|
||||
|
||||
BlinkerPauseLateralSettings::BlinkerPauseLateralSettings(const QString ¶m, const QString &title, const QString &description, const QString &icon, QWidget *parent)
|
||||
: ExpandableToggleRow(param, title, description, icon, parent) {
|
||||
|
||||
pauseLateralSpeed = new OptionControlSP("BlinkerMinLateralControlSpeed", "", "", "", {0, 255}, 5);
|
||||
connect(pauseLateralSpeed, &OptionControlSP::updateLabels, this, &BlinkerPauseLateralSettings::refresh);
|
||||
addItem(pauseLateralSpeed);
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void BlinkerPauseLateralSettings::refresh() {
|
||||
const QString option = QString::fromStdString(params.get("BlinkerMinLateralControlSpeed"));
|
||||
const bool is_metric = params.getBool("IsMetric");
|
||||
const QString unit = is_metric ? "km/h" : "mph";
|
||||
|
||||
pauseLateralSpeed->setLabel(option + " " + unit);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/expandable_row.h"
|
||||
|
||||
class BlinkerPauseLateralSettings : public ExpandableToggleRow {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
BlinkerPauseLateralSettings(const QString ¶m, const QString &title, const QString &description, const QString &icon, QWidget *parent = nullptr);
|
||||
void refresh();
|
||||
|
||||
private:
|
||||
Params params;
|
||||
OptionControlSP *pauseLateralSpeed;
|
||||
};
|
||||
@@ -64,6 +64,17 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) {
|
||||
list->addItem(vertical_space(0));
|
||||
list->addItem(horizontal_line());
|
||||
|
||||
// Blinker Pause Lateral Control
|
||||
blinkerPauseLateralSettings = new BlinkerPauseLateralSettings(
|
||||
"BlinkerPauseLateralControl",
|
||||
tr("Pause Lateral Control with Blinker"),
|
||||
tr("Pause lateral control with blinker when traveling below the desired speed selected."),
|
||||
"",
|
||||
this);
|
||||
list->addItem(blinkerPauseLateralSettings);
|
||||
|
||||
list->addItem(horizontal_line());
|
||||
|
||||
// Neural Network Lateral Control
|
||||
nnlcToggle = new NeuralNetworkLateralControl();
|
||||
list->addItem(nnlcToggle);
|
||||
@@ -139,5 +150,7 @@ void LateralPanel::updateToggles(bool _offroad) {
|
||||
|
||||
madsSettingsButton->setEnabled(madsToggle->isToggled());
|
||||
|
||||
blinkerPauseLateralSettings->refresh();
|
||||
|
||||
offroad = _offroad;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <string>
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.h"
|
||||
@@ -43,6 +44,7 @@ private:
|
||||
PushButtonSP *laneChangeSettingsButton;
|
||||
LaneChangeSettings *laneChangeWidget = nullptr;
|
||||
NeuralNetworkLateralControl *nnlcToggle = nullptr;
|
||||
BlinkerPauseLateralSettings *blinkerPauseLateralSettings = nullptr;
|
||||
|
||||
const QString MADS_BASE_DESC = tr("Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC).");
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.h"
|
||||
|
||||
CustomAccIncrement::CustomAccIncrement(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent)
|
||||
: ExpandableToggleRow(param, title, desc, icon, parent) {
|
||||
auto *accFrame = new QFrame(this);
|
||||
auto *accFrameLayout = new QGridLayout();
|
||||
accFrame->setLayout(accFrameLayout);
|
||||
accFrameLayout->setSpacing(0);
|
||||
|
||||
auto *shortPressControl = new AccIncrementOptionControl("CustomAccShortPressIncrement", {1, 10}, 1);
|
||||
connect(shortPressControl, &OptionControlSP::updateLabels, shortPressControl, &AccIncrementOptionControl::refresh);
|
||||
|
||||
auto *longPressControl = new AccIncrementOptionControl("CustomAccLongPressIncrement", {1, 3}, 1, &customLongValues);
|
||||
connect(longPressControl, &OptionControlSP::updateLabels, longPressControl, &AccIncrementOptionControl::refresh);
|
||||
|
||||
shortPressControl->setFixedWidth(280);
|
||||
longPressControl->setFixedWidth(280);
|
||||
accFrameLayout->addWidget(shortPressControl, 0, 0, Qt::AlignLeft);
|
||||
accFrameLayout->addWidget(longPressControl, 0, 1, Qt::AlignRight);
|
||||
|
||||
addItem(accFrame);
|
||||
}
|
||||
|
||||
AccIncrementOptionControl::AccIncrementOptionControl(const QString ¶m, const MinMaxValue &range, const int per_value_change, const QMap<QString, QString> *valMap)
|
||||
: OptionControlSP(param, "", "", "", range, per_value_change, true, valMap) {
|
||||
param_name = param.toStdString();
|
||||
refresh();
|
||||
}
|
||||
|
||||
void AccIncrementOptionControl::refresh() {
|
||||
std::string val = params.get(param_name);
|
||||
std::string label = "<span style='font-size: 45px; font-weight: 450; color: #FFFFFF;'>";
|
||||
label += param_name == "CustomAccShortPressIncrement" ? "Short Press" : "Long Press";
|
||||
label += " <br><span style='font-size: 40px; font-weight: 450; color:rgb(174, 255, 195);'>" + val;
|
||||
label += param_name == "CustomAccShortPressIncrement"
|
||||
? (val == "1" ? " (Default)" : "")
|
||||
: (val == "5" ? " (Default)" : "");
|
||||
label += "</span></span>";
|
||||
setLabel(QString::fromStdString(label));
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/expandable_row.h"
|
||||
|
||||
class CustomAccIncrement : public ExpandableToggleRow {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CustomAccIncrement(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr);
|
||||
|
||||
private:
|
||||
QMap<QString, QString> customLongValues = {
|
||||
{"1", "1"},
|
||||
{"2", "5"}, // Default
|
||||
{"3", "10"}
|
||||
};
|
||||
};
|
||||
|
||||
class AccIncrementOptionControl : public OptionControlSP {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AccIncrementOptionControl(const QString ¶m, const MinMaxValue &range, int per_value_change, const QMap<QString, QString> *valMap = nullptr);
|
||||
void refresh();
|
||||
|
||||
protected:
|
||||
std::string param_name;
|
||||
|
||||
private:
|
||||
Params params;
|
||||
};
|
||||
@@ -8,4 +8,72 @@
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h"
|
||||
|
||||
LongitudinalPanel::LongitudinalPanel(QWidget *parent) : QWidget(parent) {
|
||||
main_layout = new QStackedLayout(this);
|
||||
ListWidget *list = new ListWidget(this, false);
|
||||
|
||||
cruisePanelScreen = new QWidget(this);
|
||||
QVBoxLayout *vlayout = new QVBoxLayout(cruisePanelScreen);
|
||||
vlayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
cruisePanelScroller = new ScrollViewSP(list, this);
|
||||
vlayout->addWidget(cruisePanelScroller);
|
||||
|
||||
customAccIncrement = new CustomAccIncrement("CustomAccIncrementsEnabled", tr("Custom ACC Speed Increments"), "", "", this);
|
||||
list->addItem(customAccIncrement);
|
||||
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, this, &LongitudinalPanel::refresh);
|
||||
|
||||
main_layout->addWidget(cruisePanelScreen);
|
||||
main_layout->setCurrentWidget(cruisePanelScreen);
|
||||
refresh(offroad);
|
||||
}
|
||||
|
||||
void LongitudinalPanel::showEvent(QShowEvent *event) {
|
||||
main_layout->setCurrentWidget(cruisePanelScreen);
|
||||
refresh(offroad);
|
||||
}
|
||||
|
||||
void LongitudinalPanel::refresh(bool _offroad) {
|
||||
auto cp_bytes = params.get("CarParamsPersistent");
|
||||
if (!cp_bytes.empty()) {
|
||||
AlignedBuffer aligned_buf;
|
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size()));
|
||||
cereal::CarParams::Reader CP = cmsg.getRoot<cereal::CarParams>();
|
||||
|
||||
has_longitudinal_control = hasLongitudinalControl(CP);
|
||||
is_pcm_cruise = CP.getPcmCruise();
|
||||
} else {
|
||||
has_longitudinal_control = false;
|
||||
is_pcm_cruise = false;
|
||||
}
|
||||
|
||||
QString accEnabledDescription = tr("Enable custom Short & Long press increments for cruise speed increase/decrease.");
|
||||
QString accNoLongDescription = tr("This feature can only be used with openpilot longitudinal control enabled.");
|
||||
QString accPcmCruiseDisabledDescription = tr("This feature is not supported on this platform due to vehicle limitations.");
|
||||
QString onroadOnlyDescription = tr("Start the vehicle to check vehicle compatibility.");
|
||||
|
||||
if (offroad) {
|
||||
customAccIncrement->setDescription(onroadOnlyDescription);
|
||||
customAccIncrement->showDescription();
|
||||
} else {
|
||||
if (has_longitudinal_control) {
|
||||
if (is_pcm_cruise) {
|
||||
customAccIncrement->setDescription(accPcmCruiseDisabledDescription);
|
||||
customAccIncrement->showDescription();
|
||||
} else {
|
||||
customAccIncrement->setDescription(accEnabledDescription);
|
||||
}
|
||||
} else {
|
||||
params.remove("CustomAccIncrementsEnabled");
|
||||
customAccIncrement->toggleFlipped(false);
|
||||
customAccIncrement->setDescription(accNoLongDescription);
|
||||
customAccIncrement->showDescription();
|
||||
}
|
||||
}
|
||||
|
||||
// enable toggle when long is available and is not PCM cruise
|
||||
customAccIncrement->setEnabled(has_longitudinal_control && !is_pcm_cruise && !offroad);
|
||||
customAccIncrement->refresh();
|
||||
|
||||
offroad = _offroad;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,26 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
|
||||
|
||||
class LongitudinalPanel : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LongitudinalPanel(QWidget *parent = nullptr);
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void refresh(bool _offroad);
|
||||
|
||||
private:
|
||||
Params params;
|
||||
bool has_longitudinal_control = false;
|
||||
bool is_pcm_cruise = false;
|
||||
bool offroad = false;
|
||||
|
||||
QStackedLayout *main_layout = nullptr;
|
||||
ScrollViewSP *cruisePanelScroller = nullptr;
|
||||
QWidget *cruisePanelScreen = nullptr;
|
||||
CustomAccIncrement *customAccIncrement = nullptr;
|
||||
};
|
||||
|
||||
@@ -7,21 +7,55 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <QJsonDocument>
|
||||
#include <QStyle>
|
||||
|
||||
#include "common/model.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
|
||||
|
||||
static const QString progressStyleActive = "QProgressBar {"
|
||||
" font-size: 40px;"
|
||||
" font-weight: 200;"
|
||||
" padding: 1px;"
|
||||
" border: 3px solid black;"
|
||||
" border-radius: 10px;"
|
||||
"}"
|
||||
"QProgressBar::chunk {"
|
||||
" background-color: #1e79e8;"
|
||||
" border-radius: 10px;"
|
||||
"}";
|
||||
|
||||
static const QString progressStyleInactive = progressStyleActive +
|
||||
"QProgressBar::chunk {"
|
||||
" background-color: transparent;"
|
||||
"}";
|
||||
|
||||
static const QString progressStyleDone = progressStyleActive +
|
||||
"QProgressBar {"
|
||||
" color: #33ab4c;"
|
||||
"}"
|
||||
"QProgressBar::chunk {"
|
||||
" background-color: transparent;"
|
||||
"}";
|
||||
|
||||
static const QString progressStyleError = progressStyleActive +
|
||||
"QProgressBar {"
|
||||
" color: red;"
|
||||
"}"
|
||||
"QProgressBar::chunk {"
|
||||
" background-color: transparent;"
|
||||
"}";
|
||||
|
||||
ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
main_layout->setContentsMargins(50, 20, 50, 20);
|
||||
|
||||
ListWidgetSP *list = new ListWidgetSP(this);
|
||||
ListWidgetSP *list = new ListWidgetSP(this, false);
|
||||
ScrollViewSP *scroller = new ScrollViewSP(list, this);
|
||||
main_layout->addWidget(scroller);
|
||||
|
||||
const auto current_model = GetActiveModelName();
|
||||
currentModelLblBtn = new ButtonControlSP(tr("Current Model"), tr("SELECT"), current_model);
|
||||
currentModelLblBtn = new ButtonControlSP(tr("Current Model"), tr("SELECT"), "", this);
|
||||
currentModelLblBtn->setValue(current_model);
|
||||
|
||||
connect(currentModelLblBtn, &ButtonControlSP::clicked, this, &ModelsPanel::handleCurrentModelLblBtnClicked);
|
||||
@@ -31,17 +65,70 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) {
|
||||
});
|
||||
connect(uiStateSP(), &UIStateSP::uiUpdate, this, &ModelsPanel::updateLabels);
|
||||
list->addItem(currentModelLblBtn);
|
||||
|
||||
// Create progress bars for downloads
|
||||
supercomboProgressBar = createProgressBar(this);
|
||||
QString supercomboType = tr("Driving Model");
|
||||
supercomboFrame = createModelDetailFrame(this, supercomboType, supercomboProgressBar);
|
||||
list->addItem(supercomboFrame);
|
||||
|
||||
navigationProgressBar = createProgressBar(this);
|
||||
QString navigationType = tr("Navigation Model");
|
||||
navigationFrame = createModelDetailFrame(this, navigationType, navigationProgressBar);
|
||||
list->addItem(navigationFrame);
|
||||
|
||||
visionProgressBar = createProgressBar(this);
|
||||
QString visionType = tr("Vision Model");
|
||||
visionFrame = createModelDetailFrame(this, visionType, visionProgressBar);
|
||||
list->addItem(visionFrame);
|
||||
|
||||
policyProgressBar = createProgressBar(this);
|
||||
QString policyType = tr("Policy Model");
|
||||
policyFrame = createModelDetailFrame(this, policyType, policyProgressBar);
|
||||
list->addItem(policyFrame);
|
||||
|
||||
list->addItem(horizontal_line());
|
||||
|
||||
// LiveDelay toggle
|
||||
list->addItem(new ParamControlSP("LagdToggle",
|
||||
tr("Live Learning Steer Delay"),
|
||||
tr("Enable this for the car to learn and adapt its steering response time. "
|
||||
"Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience."),
|
||||
"../assets/offroad/icon_shell.png"));
|
||||
}
|
||||
|
||||
QProgressBar* ModelsPanel::createProgressBar(QWidget *parent) {
|
||||
QProgressBar *progressBar = new QProgressBar(parent);
|
||||
progressBar->setRange(0, 100);
|
||||
progressBar->setValue(0);
|
||||
progressBar->setTextVisible(true);
|
||||
progressBar->setAlignment(Qt::AlignVCenter);
|
||||
return progressBar;
|
||||
}
|
||||
|
||||
QFrame* ModelsPanel::createModelDetailFrame(QWidget *parent, QString &typeName, QProgressBar *progressBar) {
|
||||
QFrame *frame = new QFrame(parent);
|
||||
QHBoxLayout *layout = new QHBoxLayout(frame);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(50);
|
||||
layout->addWidget(new QLabel(typeName));
|
||||
layout->addWidget(progressBar);
|
||||
frame->setVisible(false);
|
||||
return frame;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Updates the UI with bundle download progress information
|
||||
* Reads status from modelManagerSP cereal message and displays status for all models
|
||||
*/
|
||||
void ModelsPanel::handleBundleDownloadProgress() {
|
||||
supercomboFrame->setVisible(false);
|
||||
visionFrame->setVisible(false);
|
||||
policyFrame->setVisible(false);
|
||||
navigationFrame->setVisible(false);
|
||||
|
||||
using DS = cereal::ModelManagerSP::DownloadStatus;
|
||||
if (!model_manager.hasSelectedBundle() && !model_manager.hasActiveBundle()) {
|
||||
currentModelLblBtn->setDescription(tr("No custom model selected!"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -54,21 +141,27 @@ void ModelsPanel::handleBundleDownloadProgress() {
|
||||
|
||||
// Get status for each model type in order
|
||||
for (const auto &model: models) {
|
||||
QString typeName;
|
||||
QString modelName = QString::fromStdString(bundle.getDisplayName());
|
||||
|
||||
QProgressBar *progressBar = nullptr;
|
||||
QFrame *modelFrame = nullptr;
|
||||
|
||||
switch (model.getType()) {
|
||||
case cereal::ModelManagerSP::Model::Type::SUPERCOMBO:
|
||||
typeName = tr("Driving");
|
||||
progressBar = supercomboProgressBar;
|
||||
modelFrame = supercomboFrame;
|
||||
break;
|
||||
case cereal::ModelManagerSP::Model::Type::NAVIGATION:
|
||||
typeName = tr("Navigation");
|
||||
progressBar = navigationProgressBar;
|
||||
modelFrame = navigationFrame;
|
||||
break;
|
||||
case cereal::ModelManagerSP::Model::Type::VISION:
|
||||
typeName = tr("Vision");
|
||||
progressBar = visionProgressBar;
|
||||
modelFrame = visionFrame;
|
||||
break;
|
||||
case cereal::ModelManagerSP::Model::Type::POLICY:
|
||||
typeName = tr("Policy");
|
||||
progressBar = policyProgressBar;
|
||||
modelFrame = policyFrame;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -76,31 +169,26 @@ void ModelsPanel::handleBundleDownloadProgress() {
|
||||
QString line;
|
||||
|
||||
if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING) {
|
||||
line = tr("Downloading %1 model [%2]... (%3%)").arg(typeName, modelName).arg(progress.getProgress(), 0, 'f', 2);
|
||||
progressBar->setStyleSheet(progressStyleActive);
|
||||
progressBar->setValue(progress.getProgress());
|
||||
progressBar->setFormat(QString(" %1% - %2").arg(static_cast<int>(progress.getProgress())).arg(modelName));
|
||||
device()->resetInteractiveTimeout();
|
||||
} else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADED) {
|
||||
line = tr("%1 model [%2] %3").arg(typeName, modelName, download_status_changed ? tr("downloaded") : tr("ready"));
|
||||
progressBar->setStyleSheet(progressStyleDone);
|
||||
progressBar->setFormat(tr(" %1 - %2").arg(modelName, download_status_changed ? tr("downloaded") : tr("ready")));
|
||||
} else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::CACHED) {
|
||||
line = tr("%1 model [%2] %3").arg(typeName, modelName, download_status_changed ? tr("from cache") : tr("ready"));
|
||||
progressBar->setStyleSheet(progressStyleDone);
|
||||
progressBar->setFormat(tr(" %1 - %2").arg(modelName, download_status_changed ? tr("from cache") : tr("ready")));
|
||||
} else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::FAILED) {
|
||||
line = tr("%1 model [%2] download failed").arg(typeName, modelName);
|
||||
progressBar->setStyleSheet(progressStyleError);
|
||||
progressBar->setFormat(tr(" download failed - %1").arg(modelName));
|
||||
} else {
|
||||
line = tr("%1 model [%2] pending...").arg(typeName, modelName);
|
||||
progressBar->setStyleSheet(progressStyleInactive);
|
||||
progressBar->setFormat(tr(" pending - %1").arg(modelName));
|
||||
}
|
||||
status.append(line);
|
||||
}
|
||||
|
||||
currentModelLblBtn->setDescription(status.join("\n"));
|
||||
|
||||
if (prev_download_status != download_status) {
|
||||
switch (bundle.getStatus()) {
|
||||
case cereal::ModelManagerSP::DownloadStatus::DOWNLOADING:
|
||||
case cereal::ModelManagerSP::DownloadStatus::CACHED:
|
||||
case cereal::ModelManagerSP::DownloadStatus::DOWNLOADED:
|
||||
currentModelLblBtn->showDescription();
|
||||
break;
|
||||
case cereal::ModelManagerSP::DownloadStatus::FAILED:
|
||||
default:
|
||||
break;
|
||||
// keep navigation hidden for now to avoid confusion
|
||||
if (model.getType() != cereal::ModelManagerSP::Model::Type::NAVIGATION) {
|
||||
modelFrame->setVisible(true);
|
||||
}
|
||||
}
|
||||
prev_download_status = download_status;
|
||||
@@ -118,6 +206,17 @@ QString ModelsPanel::GetActiveModelName() {
|
||||
return DEFAULT_MODEL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gets the short name of the currently selected model bundle
|
||||
* @return Display short name of the selected bundle or default model name
|
||||
*/
|
||||
QString ModelsPanel::GetActiveModelInternalName() {
|
||||
if (model_manager.hasActiveBundle()) {
|
||||
return QString::fromStdString(model_manager.getActiveBundle().getInternalName());
|
||||
}
|
||||
return DEFAULT_MODEL;
|
||||
}
|
||||
|
||||
void ModelsPanel::updateModelManagerState() {
|
||||
const SubMaster &sm = *(uiStateSP()->sm);
|
||||
model_manager = sm["modelManagerSP"].getModelManagerSP();
|
||||
@@ -141,7 +240,7 @@ void ModelsPanel::handleCurrentModelLblBtnClicked() {
|
||||
// Sort bundles by index in descending order
|
||||
QStringList bundleNames;
|
||||
// Add "Default" as the first option
|
||||
bundleNames.append(tr("Use Default"));
|
||||
bundleNames.append(DEFAULT_MODEL);
|
||||
|
||||
auto indices = index_to_bundle.keys();
|
||||
std::sort(indices.begin(), indices.end(), std::greater<uint32_t>());
|
||||
@@ -149,7 +248,7 @@ void ModelsPanel::handleCurrentModelLblBtnClicked() {
|
||||
bundleNames.append(index_to_bundle[index]);
|
||||
}
|
||||
|
||||
currentModelLblBtn->setValue(GetActiveModelName());
|
||||
currentModelLblBtn->setValue(GetActiveModelInternalName());
|
||||
|
||||
const QString selectedBundleName = MultiOptionDialog::getSelection(
|
||||
tr("Select a Model"), bundleNames, GetActiveModelName(), this);
|
||||
@@ -159,7 +258,7 @@ void ModelsPanel::handleCurrentModelLblBtnClicked() {
|
||||
}
|
||||
|
||||
// Handle "Stock" selection differently
|
||||
if (selectedBundleName == tr("Use Default")) {
|
||||
if (selectedBundleName == DEFAULT_MODEL) {
|
||||
params.remove("ModelManager_ActiveBundle");
|
||||
currentModelLblBtn->setValue(tr("Default"));
|
||||
showResetParamsDialog();
|
||||
@@ -190,7 +289,7 @@ void ModelsPanel::updateLabels() {
|
||||
updateModelManagerState();
|
||||
handleBundleDownloadProgress();
|
||||
currentModelLblBtn->setEnabled(!is_onroad && !isDownloading());
|
||||
currentModelLblBtn->setValue(GetActiveModelName());
|
||||
currentModelLblBtn->setValue(GetActiveModelInternalName());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user