mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-08-07 06:35:41 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 258205e9f7 | |||
| fe46463c66 | |||
| 888c9e9620 | |||
| baac950325 |
@@ -18,19 +18,6 @@
|
|||||||
|
|
||||||
venv/
|
venv/
|
||||||
.venv/
|
.venv/
|
||||||
**/.idea
|
|
||||||
**/.hypothesis
|
|
||||||
**/.mypy_cache
|
|
||||||
|
|
||||||
**/.venv
|
|
||||||
**/.venv/
|
|
||||||
|
|
||||||
**/.ci_cache
|
|
||||||
**/*.rlog
|
|
||||||
|
|
||||||
**/Dockerfile*
|
|
||||||
**/dockerfile*
|
|
||||||
**/build_output
|
|
||||||
|
|
||||||
notebooks
|
notebooks
|
||||||
phone
|
phone
|
||||||
|
|||||||
@@ -54,9 +54,8 @@ jobs:
|
|||||||
git add .
|
git add .
|
||||||
- name: update car docs
|
- name: update car docs
|
||||||
run: |
|
run: |
|
||||||
export PYTHONPATH="$PWD"
|
|
||||||
scons -j$(nproc) --minimal opendbc_repo
|
scons -j$(nproc) --minimal opendbc_repo
|
||||||
python selfdrive/car/docs.py
|
PYTHONPATH=. python selfdrive/car/docs.py
|
||||||
git add docs/CARS.md
|
git add docs/CARS.md
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
|
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ FROM ghcr.io/commaai/openpilot-base:latest
|
|||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
ENV OPENPILOT_PATH=/home/batman/openpilot
|
ENV OPENPILOT_PATH=/home/batman/openpilot
|
||||||
|
ENV PYTHONPATH=${OPENPILOT_PATH}:${PYTHONPATH}
|
||||||
|
|
||||||
RUN mkdir -p ${OPENPILOT_PATH}
|
RUN mkdir -p ${OPENPILOT_PATH}
|
||||||
WORKDIR ${OPENPILOT_PATH}
|
WORKDIR ${OPENPILOT_PATH}
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
FROM sunnypilot-base
|
|
||||||
|
|
||||||
ARG RUNNER_DEBUG=0
|
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
|
||||||
ENV OPENPILOT_SRC_PATH=/tmp/openpilot
|
|
||||||
ENV BUILD_DIR=/data/openpilot
|
|
||||||
ENV OUTPUT_DIR=/output
|
|
||||||
|
|
||||||
RUN sudo apt update && sudo apt install -y rsync
|
|
||||||
|
|
||||||
RUN mkdir -p ${OPENPILOT_SRC_PATH}
|
|
||||||
RUN mkdir -p ${BUILD_DIR}
|
|
||||||
COPY . ${OPENPILOT_SRC_PATH}
|
|
||||||
ENV PYTHONPATH=${BUILD_DIR}
|
|
||||||
|
|
||||||
WORKDIR ${OPENPILOT_SRC_PATH}
|
|
||||||
RUN ./tools/ubuntu_setup.sh
|
|
||||||
|
|
||||||
RUN ./release/release_files.py | sort | uniq | rsync -rRl${RUNNER_DEBUG:+v} --files-from=- . $BUILD_DIR/
|
|
||||||
WORKDIR ${BUILD_DIR}
|
|
||||||
RUN sed -i '/from .board.jungle import PandaJungle, PandaJungleDFU/s/^/#/' panda/__init__.py
|
|
||||||
RUN scons --cache-readonly -j$(nproc) --minimal
|
|
||||||
RUN touch ${BUILD_DIR}/prebuilt
|
|
||||||
RUN sudo rm -rf ${OUTPUT_DIR}
|
|
||||||
RUN mkdir -p ${OUTPUT_DIR}
|
|
||||||
|
|
||||||
ENTRYPOINT [\
|
|
||||||
"rsync", \
|
|
||||||
"-am", \
|
|
||||||
"--include=**/panda/board/", \
|
|
||||||
"--include=**/panda/board/obj", \
|
|
||||||
"--include=**/panda/board/obj/panda.bin.signed", \
|
|
||||||
"--include=**/panda/board/obj/panda_h7.bin.signed", \
|
|
||||||
"--include=**/panda/board/obj/bootstub.panda.bin", \
|
|
||||||
"--include=**/panda/board/obj/bootstub.panda_h7.bin", \
|
|
||||||
"--exclude=.sconsign.dblite", \
|
|
||||||
"--exclude=*.a", \
|
|
||||||
"--exclude=*.o", \
|
|
||||||
"--exclude=*.os", \
|
|
||||||
"--exclude=*.pyc", \
|
|
||||||
"--exclude=moc_*", \
|
|
||||||
"--exclude=*.cc", \
|
|
||||||
"--exclude=Jenkinsfile", \
|
|
||||||
"--exclude=supercombo.onnx", \
|
|
||||||
"--exclude=**/panda/board/*", \
|
|
||||||
"--exclude=**/panda/board/obj/**", \
|
|
||||||
"--exclude=**/panda/certs/", \
|
|
||||||
"--exclude=**/panda/crypto/", \
|
|
||||||
"--exclude=**/release/", \
|
|
||||||
"--exclude=**/.github/", \
|
|
||||||
"--exclude=**/selfdrive/ui/replay/", \
|
|
||||||
"--exclude=**/__pycache__/", \
|
|
||||||
"--exclude=**/selfdrive/ui/*.h", \
|
|
||||||
"--exclude=**/selfdrive/ui/**/*.h", \
|
|
||||||
"--exclude=**/selfdrive/ui/qt/offroad/sunnypilot/", \
|
|
||||||
#"--exclude=${SCONS_CACHE_DIR:-}", \
|
|
||||||
"--exclude=**/.git/", \
|
|
||||||
"--exclude=**/SConstruct", \
|
|
||||||
"--exclude=**/SConscript", \
|
|
||||||
"--exclude=**/.venv/", \
|
|
||||||
"--delete-excluded", \
|
|
||||||
"--chown=1000:1000", \
|
|
||||||
"/data/openpilot/", \
|
|
||||||
"/output/" \
|
|
||||||
]
|
|
||||||
@@ -2281,7 +2281,6 @@ struct LiveTorqueParametersData {
|
|||||||
points @10 :List(List(Float32));
|
points @10 :List(List(Float32));
|
||||||
version @11 :Int32;
|
version @11 :Int32;
|
||||||
useParams @12 :Bool;
|
useParams @12 :Bool;
|
||||||
calPerc @13 :Int8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LiveDelayData {
|
struct LiveDelayData {
|
||||||
@@ -2292,7 +2291,6 @@ struct LiveDelayData {
|
|||||||
lateralDelayEstimate @3 :Float32;
|
lateralDelayEstimate @3 :Float32;
|
||||||
lateralDelayEstimateStd @5 :Float32;
|
lateralDelayEstimateStd @5 :Float32;
|
||||||
points @4 :List(Float32);
|
points @4 :List(Float32);
|
||||||
calPerc @6 :Int8;
|
|
||||||
|
|
||||||
enum Status {
|
enum Status {
|
||||||
unestimated @0;
|
unestimated @0;
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
#define DEFAULT_MODEL "Vegetarian Filet o Fish (Default)"
|
#define DEFAULT_MODEL "Filet o Fish (Default)"
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
#!/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")
|
|
||||||
+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 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|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|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-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|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|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|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>|||
|
|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>||
|
|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,7 +38,6 @@ 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 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 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|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 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|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|
|
|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|
|
||||||
@@ -54,7 +53,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 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 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|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-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|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|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|
|
|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 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>|||
|
|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>|||
|
||||||
@@ -78,8 +77,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 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 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 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 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 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 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>|||
|
|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>|||
|
||||||
@@ -116,6 +115,7 @@ 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 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|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 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 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 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>||
|
|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>|||
|
|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|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 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-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 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 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 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|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|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 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-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|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|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|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 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>|||
|
|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|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-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|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-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|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|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 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 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|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-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 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 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|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>|||
|
|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,6 +7,7 @@ Development is coordinated through [Discord](https://discord.comma.ai) and GitHu
|
|||||||
### Getting Started
|
### Getting Started
|
||||||
|
|
||||||
* Setup your [development environment](../tools/)
|
* Setup your [development environment](../tools/)
|
||||||
|
* Read about the [development workflow](WORKFLOW.md)
|
||||||
* Join our [Discord](https://discord.comma.ai)
|
* Join our [Discord](https://discord.comma.ai)
|
||||||
* Docs are at https://docs.comma.ai and https://blog.comma.ai
|
* Docs are at https://docs.comma.ai and https://blog.comma.ai
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# 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
|
export VECLIB_MAXIMUM_THREADS=1
|
||||||
|
|
||||||
if [ -z "$AGNOS_VERSION" ]; then
|
if [ -z "$AGNOS_VERSION" ]; then
|
||||||
export AGNOS_VERSION="12.4"
|
export AGNOS_VERSION="12.3"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export STAGING_ROOT="/data/safe_staging"
|
export STAGING_ROOT="/data/safe_staging"
|
||||||
|
|||||||
+1
-1
Submodule opendbc_repo updated: c87940a6b5...3bd2bba8a7
+1
-1
Submodule panda updated: c33cfa0803...5ac4fa5bb0
+3
-8
@@ -59,7 +59,7 @@ dependencies = [
|
|||||||
"future-fstrings",
|
"future-fstrings",
|
||||||
|
|
||||||
# joystickd
|
# joystickd
|
||||||
"inputs",
|
"pygame",
|
||||||
|
|
||||||
# these should be removed
|
# these should be removed
|
||||||
"psutil",
|
"psutil",
|
||||||
@@ -68,9 +68,6 @@ dependencies = [
|
|||||||
|
|
||||||
# logreader
|
# logreader
|
||||||
"zstandard",
|
"zstandard",
|
||||||
|
|
||||||
# ui
|
|
||||||
"qrcode",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
@@ -88,8 +85,7 @@ testing = [
|
|||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-cpp",
|
"pytest-cpp",
|
||||||
"pytest-subtests",
|
"pytest-subtests",
|
||||||
# https://github.com/pytest-dev/pytest-xdist/issues/1215
|
"pytest-xdist",
|
||||||
"pytest-xdist @ git+https://github.com/sshane/pytest-xdist@909e97b49d12401c10608f9d777bfc9dab8a4413",
|
|
||||||
"pytest-timeout",
|
"pytest-timeout",
|
||||||
"pytest-randomly",
|
"pytest-randomly",
|
||||||
"pytest-asyncio",
|
"pytest-asyncio",
|
||||||
@@ -106,11 +102,10 @@ dev = [
|
|||||||
"azure-storage-blob",
|
"azure-storage-blob",
|
||||||
"dbus-next",
|
"dbus-next",
|
||||||
"dictdiffer",
|
"dictdiffer",
|
||||||
|
"lru-dict",
|
||||||
"matplotlib",
|
"matplotlib",
|
||||||
"opencv-python-headless",
|
|
||||||
"parameterized >=0.8, <0.9",
|
"parameterized >=0.8, <0.9",
|
||||||
"pyautogui",
|
"pyautogui",
|
||||||
"pygame",
|
|
||||||
"pyopencl; platform_machine != 'aarch64'", # broken on arm64
|
"pyopencl; platform_machine != 'aarch64'", # broken on arm64
|
||||||
"pytools < 2024.1.11; platform_machine != 'aarch64'", # pyopencl use a broken version
|
"pytools < 2024.1.11; platform_machine != 'aarch64'", # pyopencl use a broken version
|
||||||
"pywinctl",
|
"pywinctl",
|
||||||
|
|||||||
+1
-1
Submodule rednose_repo updated: 7fddc8e6d4...8b86052919
+20
-15
@@ -1,31 +1,36 @@
|
|||||||
# openpilot releases
|
# openpilot releases
|
||||||
|
|
||||||
```
|
|
||||||
## release checklist
|
## release checklist
|
||||||
|
|
||||||
**Go to `devel-staging`**
|
**Go to `devel-staging`**
|
||||||
- [ ] update RELEASES.md
|
|
||||||
- [ ] update `devel-staging`: `git reset --hard origin/master-ci`
|
- [ ] update `devel-staging`: `git reset --hard origin/master-ci`
|
||||||
- [ ] open a pull request from `devel-staging` to `devel`
|
- [ ] open a pull request from `devel-staging` to `devel`
|
||||||
- [ ] post on Discord
|
|
||||||
|
|
||||||
**Go to `devel`**
|
**Go to `devel`**
|
||||||
|
- [ ] update RELEASES.md
|
||||||
|
- [ ] close out milestone
|
||||||
|
- [ ] post on Discord dev channel
|
||||||
- [ ] bump version on master: `common/version.h` and `RELEASES.md`
|
- [ ] bump version on master: `common/version.h` and `RELEASES.md`
|
||||||
- [ ] before merging the pull request
|
- [ ] merge the pull request
|
||||||
- [ ] update from previous release -> new release
|
|
||||||
- [ ] update from new release -> previous release
|
tests:
|
||||||
- [ ] fresh install with `openpilot-test.comma.ai`
|
- [ ] update from previous release -> new release
|
||||||
- [ ] drive on fresh install
|
- [ ] update from new release -> previous release
|
||||||
- [ ] no submodules or LFS
|
- [ ] fresh install with `openpilot-test.comma.ai`
|
||||||
- [ ] check sentry, MTBF, etc.
|
- [ ] drive on fresh install
|
||||||
|
- [ ] comma body test
|
||||||
|
- [ ] no submodules or LFS
|
||||||
|
- [ ] check sentry, MTBF, etc.
|
||||||
|
|
||||||
**Go to `release3`**
|
**Go to `release3`**
|
||||||
- [ ] publish the blog post
|
- [ ] publish the blog post
|
||||||
- [ ] `git reset --hard origin/release3-staging`
|
- [ ] `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
|
- [ ] create GitHub release
|
||||||
- [ ] final test install on `openpilot.comma.ai`
|
- [ ] final test install on `openpilot.comma.ai`
|
||||||
- [ ] update factory provisioning
|
- [ ] update production
|
||||||
- [ ] close out milestone
|
- [ ] Post on Discord, X, etc.
|
||||||
- [ ] post on Discord, X, etc.
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# run_openpilot_docker.sh
|
|
||||||
# POSIX-compliant script to run openpilot in Docker for local testing
|
|
||||||
|
|
||||||
# === Configurable Variables ===
|
|
||||||
|
|
||||||
# Base image to use (required)
|
|
||||||
BASE_IMAGE="${BASE_IMAGE:-commaai/openpilot-base:latest}"
|
|
||||||
|
|
||||||
# Working directory inside the container
|
|
||||||
WORKDIR="/tmp/openpilot"
|
|
||||||
|
|
||||||
# Local project path
|
|
||||||
LOCAL_DIR="$PWD"
|
|
||||||
|
|
||||||
# Shared memory size (adjust for large builds/tests)
|
|
||||||
SHM_SIZE="2G"
|
|
||||||
|
|
||||||
# Environment configuration
|
|
||||||
CI=1
|
|
||||||
PYTHONWARNINGS="error"
|
|
||||||
FILEREADER_CACHE=1
|
|
||||||
PYTHONPATH="$WORKDIR"
|
|
||||||
|
|
||||||
# Optional: GitHub Actions env vars — set them only if needed for local mirroring/debug
|
|
||||||
USE_GITHUB_ENV_VARS=false # set to true to enable GitHub-related mounts/envs
|
|
||||||
GITHUB_WORKSPACE="${GITHUB_WORKSPACE:-$HOME/openpilot_ci}" # fallback path
|
|
||||||
|
|
||||||
# === Docker Command ===
|
|
||||||
|
|
||||||
docker run --rm \
|
|
||||||
--shm-size "$SHM_SIZE" \
|
|
||||||
-v "$LOCAL_DIR":"$WORKDIR" \
|
|
||||||
-w "$WORKDIR" \
|
|
||||||
-e CI="$CI" \
|
|
||||||
-e PYTHONWARNINGS="$PYTHONWARNINGS" \
|
|
||||||
-e FILEREADER_CACHE="$FILEREADER_CACHE" \
|
|
||||||
-e PYTHONPATH="$PYTHONPATH" \
|
|
||||||
${USE_GITHUB_ENV_VARS:+\
|
|
||||||
-e NUM_JOBS \
|
|
||||||
-e JOB_ID \
|
|
||||||
-e GITHUB_ACTION \
|
|
||||||
-e GITHUB_REF \
|
|
||||||
-e GITHUB_HEAD_REF \
|
|
||||||
-e GITHUB_SHA \
|
|
||||||
-e GITHUB_REPOSITORY \
|
|
||||||
-e GITHUB_RUN_ID \
|
|
||||||
-v "$GITHUB_WORKSPACE/.ci_cache/scons_cache":/tmp/scons_cache \
|
|
||||||
-v "$GITHUB_WORKSPACE/.ci_cache/comma_download_cache":/tmp/comma_download_cache \
|
|
||||||
-v "$GITHUB_WORKSPACE/.ci_cache/openpilot_cache":/tmp/openpilot_cache } \
|
|
||||||
"$BASE_IMAGE" /bin/bash -c "${1:-/bin/bash}"
|
|
||||||
@@ -12,10 +12,11 @@
|
|||||||
|
|
||||||
<h5>Quectel/EG25-G</h5>
|
<h5>Quectel/EG25-G</h5>
|
||||||
<p>FCC ID: XMR201903EG25G</p>
|
<p>FCC ID: XMR201903EG25G</p>
|
||||||
<p>This device complies with Part 15 of the FCC Rules.</p>
|
<p>
|
||||||
<p>Operation is subject to the following two conditions:</p>
|
This device complies with Part 15 of the FCC Rules.
|
||||||
|
Operation is subject to the following two conditions:
|
||||||
|
|
||||||
<p>(1) this device may not cause harmful interference, and</p>
|
<p>(1) this device may not cause harmful interference, and
|
||||||
<p>(2) this device must accept any interference received, including interference that may cause undesired operation.</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:
|
The following test reports are subject to this declaration:
|
||||||
|
|||||||
Executable
+81
@@ -0,0 +1,81 @@
|
|||||||
|
#!/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,7 +13,6 @@ from typing import NoReturn
|
|||||||
|
|
||||||
from cereal import log, car
|
from cereal import log, car
|
||||||
import cereal.messaging as messaging
|
import cereal.messaging as messaging
|
||||||
from openpilot.system.hardware import HARDWARE
|
|
||||||
from openpilot.common.conversions import Conversions as CV
|
from openpilot.common.conversions import Conversions as CV
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.common.realtime import config_realtime_process
|
from openpilot.common.realtime import config_realtime_process
|
||||||
@@ -37,11 +36,8 @@ RPY_INIT = np.array([0.0,0.0,0.0])
|
|||||||
WIDE_FROM_DEVICE_EULER_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])
|
HEIGHT_INIT = np.array([1.22])
|
||||||
|
|
||||||
# These values are needed to accommodate the model frame in the narrow cam
|
# These values are needed to accommodate the model frame in the narrow cam of the C3
|
||||||
if HARDWARE.get_device_type() == 'mici':
|
PITCH_LIMITS = np.array([-0.09074112085129739, 0.17])
|
||||||
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])
|
YAW_LIMITS = np.array([-0.06912048084718224, 0.06912048084718235])
|
||||||
DEBUG = os.getenv("DEBUG") is not None
|
DEBUG = os.getenv("DEBUG") is not None
|
||||||
|
|
||||||
|
|||||||
@@ -82,12 +82,6 @@ class PointBuckets:
|
|||||||
total_points_valid = self.__len__() >= self.min_points_total
|
total_points_valid = self.__len__() >= self.min_points_total
|
||||||
return individual_buckets_valid and total_points_valid
|
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:
|
def is_calculable(self) -> bool:
|
||||||
return all(len(v) > 0 for v in self.buckets.values())
|
return all(len(v) > 0 for v in self.buckets.values())
|
||||||
|
|
||||||
|
|||||||
@@ -229,8 +229,6 @@ class LateralLagEstimator:
|
|||||||
liveDelay.lateralDelayEstimateStd = 0.0
|
liveDelay.lateralDelayEstimateStd = 0.0
|
||||||
|
|
||||||
liveDelay.validBlocks = self.block_avg.valid_blocks
|
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:
|
if debug:
|
||||||
liveDelay.points = self.block_avg.values.flatten().tolist()
|
liveDelay.points = self.block_avg.values.flatten().tolist()
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ class TestLagd:
|
|||||||
assert np.allclose(msg.liveDelay.lateralDelay, estimator.initial_lag)
|
assert np.allclose(msg.liveDelay.lateralDelay, estimator.initial_lag)
|
||||||
assert np.allclose(msg.liveDelay.lateralDelayEstimate, estimator.initial_lag)
|
assert np.allclose(msg.liveDelay.lateralDelayEstimate, estimator.initial_lag)
|
||||||
assert msg.liveDelay.validBlocks == 0
|
assert msg.liveDelay.validBlocks == 0
|
||||||
assert msg.liveDelay.calPerc == 0
|
|
||||||
|
|
||||||
def test_estimator_basics(self, subtests):
|
def test_estimator_basics(self, subtests):
|
||||||
for lag_frames in range(5):
|
for lag_frames in range(5):
|
||||||
@@ -108,7 +107,6 @@ class TestLagd:
|
|||||||
assert np.allclose(msg.liveDelay.lateralDelayEstimate, 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 np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
|
||||||
assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED
|
assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED
|
||||||
assert msg.liveDelay.calPerc == 100
|
|
||||||
|
|
||||||
def test_disabled_estimator(self):
|
def test_disabled_estimator(self):
|
||||||
mocked_CP = car.CarParams(steerActuatorDelay=0.8)
|
mocked_CP = car.CarParams(steerActuatorDelay=0.8)
|
||||||
@@ -121,7 +119,6 @@ class TestLagd:
|
|||||||
assert np.allclose(msg.liveDelay.lateralDelayEstimate, 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 np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
|
||||||
assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED
|
assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED
|
||||||
assert msg.liveDelay.calPerc == 100
|
|
||||||
|
|
||||||
def test_estimator_masking(self):
|
def test_estimator_masking(self):
|
||||||
mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.8), random.randint(1, 19)
|
mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.8), random.randint(1, 19)
|
||||||
@@ -130,7 +127,6 @@ class TestLagd:
|
|||||||
msg = estimator.get_msg(True)
|
msg = estimator.get_msg(True)
|
||||||
assert np.allclose(msg.liveDelay.lateralDelayEstimate, 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 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.skipif(PC, reason="only on device")
|
||||||
@pytest.mark.timeout(60)
|
@pytest.mark.timeout(60)
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
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,7 +233,6 @@ class TorqueEstimator(ParameterEstimator):
|
|||||||
liveTorqueParameters.latAccelOffsetFiltered = float(self.filtered_params['latAccelOffset'].x)
|
liveTorqueParameters.latAccelOffsetFiltered = float(self.filtered_params['latAccelOffset'].x)
|
||||||
liveTorqueParameters.frictionCoefficientFiltered = float(self.filtered_params['frictionCoefficient'].x)
|
liveTorqueParameters.frictionCoefficientFiltered = float(self.filtered_params['frictionCoefficient'].x)
|
||||||
liveTorqueParameters.totalBucketPoints = len(self.filtered_points)
|
liveTorqueParameters.totalBucketPoints = len(self.filtered_points)
|
||||||
liveTorqueParameters.calPerc = self.filtered_points.get_valid_percent()
|
|
||||||
liveTorqueParameters.decay = self.decay
|
liveTorqueParameters.decay = self.decay
|
||||||
liveTorqueParameters.maxResets = self.resets
|
liveTorqueParameters.maxResets = self.resets
|
||||||
return msg
|
return msg
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ import subprocess
|
|||||||
from tinygrad import Device
|
from tinygrad import Device
|
||||||
|
|
||||||
# because tg doesn't support multi-process
|
# 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, cwd=env.Dir('#').abspath)
|
devs = subprocess.check_output('python3 -c "from tinygrad import Device; print(list(Device.get_available_devices()))"', shell=True)
|
||||||
if b"AMD" in devs:
|
if b"AMD" in devs:
|
||||||
del Device
|
del Device
|
||||||
print("USB GPU detected... building")
|
print("USB GPU detected... building")
|
||||||
|
|||||||
+21
-20
@@ -86,20 +86,10 @@ class ModelState:
|
|||||||
prev_desire: np.ndarray # for tracking the rising edge of the pulse
|
prev_desire: np.ndarray # for tracking the rising edge of the pulse
|
||||||
|
|
||||||
def __init__(self, context: CLContext):
|
def __init__(self, context: CLContext):
|
||||||
with open(VISION_METADATA_PATH, 'rb') as f:
|
self.frames = {
|
||||||
vision_metadata = pickle.load(f)
|
'input_imgs': DrivingModelFrame(context, ModelConstants.TEMPORAL_SKIP),
|
||||||
self.vision_input_shapes = vision_metadata['input_shapes']
|
'big_input_imgs': DrivingModelFrame(context, ModelConstants.TEMPORAL_SKIP)
|
||||||
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.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)
|
self.full_features_buffer = np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32)
|
||||||
@@ -116,6 +106,18 @@ class ModelState:
|
|||||||
'features_buffer': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32),
|
'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
|
# img buffers are managed in openCL transform code
|
||||||
self.vision_inputs: dict[str, Tensor] = {}
|
self.vision_inputs: dict[str, Tensor] = {}
|
||||||
self.vision_output = np.zeros(vision_output_size, dtype=np.float32)
|
self.vision_output = np.zeros(vision_output_size, dtype=np.float32)
|
||||||
@@ -133,7 +135,7 @@ class ModelState:
|
|||||||
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
|
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
|
||||||
return parsed_model_outputs
|
return parsed_model_outputs
|
||||||
|
|
||||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray,
|
||||||
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
|
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
|
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
||||||
inputs['desire'][0] = 0
|
inputs['desire'][0] = 0
|
||||||
@@ -146,7 +148,8 @@ class ModelState:
|
|||||||
|
|
||||||
self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention']
|
self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention']
|
||||||
self.numpy_inputs['lateral_control_params'][:] = inputs['lateral_control_params']
|
self.numpy_inputs['lateral_control_params'][:] = inputs['lateral_control_params']
|
||||||
imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.vision_input_names}
|
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())}
|
||||||
|
|
||||||
if TICI and not USBGPU:
|
if TICI and not USBGPU:
|
||||||
# The imgs tensors are backed by opencl memory, only need init once
|
# The imgs tensors are backed by opencl memory, only need init once
|
||||||
@@ -325,16 +328,14 @@ def main(demo=False):
|
|||||||
if prepare_only:
|
if prepare_only:
|
||||||
cloudlog.error(f"skipping model eval. Dropped {vipc_dropped_frames} frames")
|
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] = {
|
inputs:dict[str, np.ndarray] = {
|
||||||
'desire': vec_desire,
|
'desire': vec_desire,
|
||||||
'traffic_convention': traffic_convention,
|
'traffic_convention': traffic_convention,
|
||||||
'lateral_control_params': lateral_control_params,
|
'lateral_control_params': lateral_control_params,
|
||||||
}
|
}
|
||||||
|
|
||||||
mt1 = time.perf_counter()
|
mt1 = time.perf_counter()
|
||||||
model_output = model.run(bufs, transforms, inputs, prepare_only)
|
model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only)
|
||||||
mt2 = time.perf_counter()
|
mt2 = time.perf_counter()
|
||||||
model_execution_time = mt2 - mt1
|
model_execution_time = mt2 - mt1
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5f714fb38bcd44b5d9f44e3a8e1595e1dfdf7558f0eec3485cf3f2dbb6dc7d8d
|
oid sha256:98f0121ccb6f850077b04cc91bd33d370fc6cbdc2bd35f1ab55628a15a813f36
|
||||||
size 15971805
|
size 15966721
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3ac4867fbc618037e8d03143edbfeeae960f2025644b5dcf36c6665271b4f874
|
oid sha256:897f80d0388250f99bba69b6a8434560cc0fd83157cbeb0bc134c67fe4e64624
|
||||||
size 34883375
|
size 34882971
|
||||||
|
|||||||
@@ -585,6 +585,11 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
EventName.noGps: {
|
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: {
|
EventName.tooDistracted: {
|
||||||
|
|||||||
@@ -382,16 +382,16 @@ class SelfdriveD(CruiseHelper):
|
|||||||
if (planner_fcw or model_fcw) and not self.CP.notCar:
|
if (planner_fcw or model_fcw) and not self.CP.notCar:
|
||||||
self.events.add(EventName.fcw)
|
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
|
# TODO: fix simulator
|
||||||
if not SIMULATION or REPLAY:
|
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:
|
if self.sm['modelV2'].frameDropPerc > 20:
|
||||||
self.events.add(EventName.modeldLagging)
|
self.events.add(EventName.modeldLagging)
|
||||||
|
|
||||||
|
|||||||
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/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,6 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
import os
|
||||||
import pickle
|
|
||||||
import sys
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -190,44 +189,22 @@ def model_replay(lr, frs):
|
|||||||
print("----------------- Model Timing -----------------")
|
print("----------------- Model Timing -----------------")
|
||||||
print("------------------------------------------------")
|
print("------------------------------------------------")
|
||||||
print(tabulate(rows, header, tablefmt="simple_grid", stralign="center", numalign="center", floatfmt=".4f"))
|
print(tabulate(rows, header, tablefmt="simple_grid", stralign="center", numalign="center", floatfmt=".4f"))
|
||||||
assert timings_ok or PC
|
assert timings_ok
|
||||||
|
|
||||||
return msgs
|
return msgs
|
||||||
|
|
||||||
|
|
||||||
def get_frames():
|
|
||||||
regen_cache = "--regen-cache" in sys.argv
|
|
||||||
frames_cache = '/tmp/model_replay_cache' if PC else '/data/model_replay_cache'
|
|
||||||
os.makedirs(frames_cache, exist_ok=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__":
|
if __name__ == "__main__":
|
||||||
update = "--update" in sys.argv or (os.getenv("GIT_BRANCH", "") == 'master')
|
update = "--update" in sys.argv or (os.getenv("GIT_BRANCH", "") == 'master')
|
||||||
replay_dir = os.path.dirname(os.path.abspath(__file__))
|
replay_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
# load logs
|
# load logs
|
||||||
lr = list(LogReader(get_url(TEST_ROUTE, SEGMENT, "rlog.zst")))
|
lr = list(LogReader(get_url(TEST_ROUTE, SEGMENT, "rlog.zst")))
|
||||||
frs = get_frames()
|
frs = {
|
||||||
|
'roadCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "fcamera.hevc"), readahead=True),
|
||||||
|
'driverCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "dcamera.hevc"), readahead=True),
|
||||||
|
'wideRoadCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "ecamera.hevc"), readahead=True)
|
||||||
|
}
|
||||||
|
|
||||||
log_msgs = []
|
log_msgs = []
|
||||||
# run replays
|
# run replays
|
||||||
|
|||||||
@@ -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.migration import migrate_all
|
||||||
from openpilot.selfdrive.test.process_replay.capture import ProcessOutputCapture
|
from openpilot.selfdrive.test.process_replay.capture import ProcessOutputCapture
|
||||||
from openpilot.tools.lib.logreader import LogIterable
|
from openpilot.tools.lib.logreader import LogIterable
|
||||||
from openpilot.tools.lib.framereader import FrameReader
|
from openpilot.tools.lib.framereader import BaseFrameReader
|
||||||
|
|
||||||
# Numpy gives different results based on CPU features after version 19
|
# Numpy gives different results based on CPU features after version 19
|
||||||
NUMPY_TOLERANCE = 1e-7
|
NUMPY_TOLERANCE = 1e-7
|
||||||
@@ -209,7 +209,6 @@ class ProcessContainer:
|
|||||||
streams_metas = available_streams(all_msgs)
|
streams_metas = available_streams(all_msgs)
|
||||||
for meta in streams_metas:
|
for meta in streams_metas:
|
||||||
if meta.camera_state in self.cfg.vision_pubs:
|
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)
|
frame_size = (frs[meta.camera_state].w, frs[meta.camera_state].h)
|
||||||
vipc_server.create_buffers(meta.stream, 2, *frame_size)
|
vipc_server.create_buffers(meta.stream, 2, *frame_size)
|
||||||
vipc_server.start_listener()
|
vipc_server.start_listener()
|
||||||
@@ -225,7 +224,7 @@ class ProcessContainer:
|
|||||||
|
|
||||||
def start(
|
def start(
|
||||||
self, params_config: dict[str, Any], environ_config: dict[str, Any],
|
self, params_config: dict[str, Any], environ_config: dict[str, Any],
|
||||||
all_msgs: LogIterable, frs: dict[str, FrameReader] | None,
|
all_msgs: LogIterable, frs: dict[str, BaseFrameReader] | None,
|
||||||
fingerprint: str | None, capture_output: bool
|
fingerprint: str | None, capture_output: bool
|
||||||
):
|
):
|
||||||
with self.prefix as p:
|
with self.prefix as p:
|
||||||
@@ -267,7 +266,7 @@ class ProcessContainer:
|
|||||||
self.prefix.clean_dirs()
|
self.prefix.clean_dirs()
|
||||||
self._clean_env()
|
self._clean_env()
|
||||||
|
|
||||||
def run_step(self, msg: capnp._DynamicStructReader, frs: dict[str, FrameReader] | None) -> list[capnp._DynamicStructReader]:
|
def run_step(self, msg: capnp._DynamicStructReader, frs: dict[str, BaseFrameReader] | None) -> list[capnp._DynamicStructReader]:
|
||||||
assert self.rc and self.pm and self.sockets and self.process.proc
|
assert self.rc and self.pm and self.sockets and self.process.proc
|
||||||
|
|
||||||
output_msgs = []
|
output_msgs = []
|
||||||
@@ -297,7 +296,7 @@ class ProcessContainer:
|
|||||||
camera_state = getattr(m, m.which())
|
camera_state = getattr(m, m.which())
|
||||||
camera_meta = meta_from_camera_state(m.which())
|
camera_meta = meta_from_camera_state(m.which())
|
||||||
assert frs is not None
|
assert frs is not None
|
||||||
img = frs[m.which()].get(camera_state.frameId)
|
img = frs[m.which()].get(camera_state.frameId, pix_fmt="nv12")[0]
|
||||||
self.vipc_server.send(camera_meta.stream, img.flatten().tobytes(),
|
self.vipc_server.send(camera_meta.stream, img.flatten().tobytes(),
|
||||||
camera_state.frameId, camera_state.timestampSof, camera_state.timestampEof)
|
camera_state.frameId, camera_state.timestampSof, camera_state.timestampEof)
|
||||||
self.msg_queue = []
|
self.msg_queue = []
|
||||||
@@ -656,7 +655,7 @@ def replay_process_with_name(name: str | Iterable[str], lr: LogIterable, *args,
|
|||||||
|
|
||||||
|
|
||||||
def replay_process(
|
def replay_process(
|
||||||
cfg: ProcessConfig | Iterable[ProcessConfig], lr: LogIterable, frs: dict[str, FrameReader] = None,
|
cfg: ProcessConfig | Iterable[ProcessConfig], lr: LogIterable, frs: dict[str, BaseFrameReader] = None,
|
||||||
fingerprint: str = None, return_all_logs: bool = False, custom_params: dict[str, Any] = 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
|
captured_output_store: dict[str, dict[str, str]] = None, disable_progress: bool = False
|
||||||
) -> list[capnp._DynamicStructReader]:
|
) -> list[capnp._DynamicStructReader]:
|
||||||
@@ -684,7 +683,7 @@ def replay_process(
|
|||||||
|
|
||||||
|
|
||||||
def _replay_multi_process(
|
def _replay_multi_process(
|
||||||
cfgs: list[ProcessConfig], lr: LogIterable, frs: dict[str, FrameReader] | None, fingerprint: str | None,
|
cfgs: list[ProcessConfig], lr: LogIterable, frs: dict[str, BaseFrameReader] | None, fingerprint: str | None,
|
||||||
custom_params: dict[str, Any] | None, captured_output_store: dict[str, dict[str, str]] | None, disable_progress: bool
|
custom_params: dict[str, Any] | None, captured_output_store: dict[str, dict[str, str]] | None, disable_progress: bool
|
||||||
) -> list[capnp._DynamicStructReader]:
|
) -> list[capnp._DynamicStructReader]:
|
||||||
if fingerprint is not None:
|
if fingerprint is not None:
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
f440c9e0469d32d350aa99ddaa8f44591a2ce690
|
9e2fe2942fbf77f24bccdbef15893831f9c0b390
|
||||||
@@ -3,17 +3,40 @@ import os
|
|||||||
import argparse
|
import argparse
|
||||||
import time
|
import time
|
||||||
import capnp
|
import capnp
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
|
|
||||||
from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, FAKEDATA, ProcessConfig, replay_process, get_process_config, \
|
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
|
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.selfdrive.test.update_ci_routes import upload_route
|
||||||
from openpilot.tools.lib.framereader import FrameReader
|
from openpilot.tools.lib.framereader import FrameReader, BaseFrameReader, FrameType
|
||||||
from openpilot.tools.lib.logreader import LogReader, LogIterable, save_log
|
from openpilot.tools.lib.logreader import LogReader, LogIterable, save_log
|
||||||
from openpilot.tools.lib.openpilotci import get_url
|
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="rgb24"):
|
||||||
|
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(
|
def regen_segment(
|
||||||
lr: LogIterable, frs: dict[str, Any] = None,
|
lr: LogIterable, frs: dict[str, Any] = None,
|
||||||
@@ -41,7 +64,7 @@ def setup_data_readers(
|
|||||||
frs['wideRoadCameraState'] = FrameReader(get_url(route, str(sidx), "ecamera.hevc"))
|
frs['wideRoadCameraState'] = FrameReader(get_url(route, str(sidx), "ecamera.hevc"))
|
||||||
if needs_driver_cam:
|
if needs_driver_cam:
|
||||||
if dummy_driver_cam:
|
if dummy_driver_cam:
|
||||||
frs['driverCameraState'] = FrameReader(get_url(route, str(sidx), "fcamera.hevc")) # Use fcam as dummy
|
frs['driverCameraState'] = DummyFrameReader.zero_dcamera()
|
||||||
else:
|
else:
|
||||||
device_type = next(str(msg.initData.deviceType) for msg in lr if msg.which() == "initData")
|
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."
|
assert device_type != "neo", "Driver camera not supported on neo segments. Use dummy dcamera."
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from openpilot.tools.lib.logreader import LogReader, save_log
|
|||||||
IS_AZURE_TOKEN_DEFINED = os.getenv("AZURE_TOKEN")
|
IS_AZURE_TOKEN_DEFINED = os.getenv("AZURE_TOKEN")
|
||||||
|
|
||||||
source_segments = [
|
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
|
("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)
|
("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
|
("TOYOTA", "0982d79ebb0de295|2021-01-04--17-13-21--13"), # TOYOTA.TOYOTA_PRIUS
|
||||||
@@ -43,6 +44,7 @@ source_segments = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
segments = [
|
segments = [
|
||||||
|
("BODY", "regen2F3C7259F1B|2025-04-08--23-00-23--0"),
|
||||||
("HYUNDAI", "regenAA0FC4ED71E|2025-04-08--22-57-50--0"),
|
("HYUNDAI", "regenAA0FC4ED71E|2025-04-08--22-57-50--0"),
|
||||||
("HYUNDAI2", "regenAFB9780D823|2025-04-08--23-00-34--0"),
|
("HYUNDAI2", "regenAFB9780D823|2025-04-08--23-00-34--0"),
|
||||||
("TOYOTA", "regen218A4DCFAA1|2025-04-08--22-57-51--0"),
|
("TOYOTA", "regen218A4DCFAA1|2025-04-08--22-57-51--0"),
|
||||||
@@ -63,7 +65,7 @@ segments = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
# dashcamOnly makes don't need to be tested until a full port is done
|
# dashcamOnly makes don't need to be tested until a full port is done
|
||||||
excluded_interfaces = ["mock", "body"]
|
excluded_interfaces = ["mock", "tesla"]
|
||||||
|
|
||||||
BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/"
|
BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/"
|
||||||
REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit")
|
REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit")
|
||||||
@@ -251,7 +253,7 @@ if __name__ == "__main__":
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# to speed things up, we only test all segments on card
|
# to speed things up, we only test all segments on card
|
||||||
if cfg.proc_name not in ('card', 'controlsd', 'lagd') and car_brand not in ('HYUNDAI', 'TOYOTA'):
|
if cfg.proc_name != 'card' and car_brand not in ('HYUNDAI', 'TOYOTA', 'HONDA', 'SUBARU', 'FORD', 'RIVIAN', 'TESLA'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst")
|
cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from parameterized import parameterized
|
from parameterized import parameterized
|
||||||
|
|
||||||
from openpilot.selfdrive.test.process_replay.regen import regen_segment
|
from openpilot.selfdrive.test.process_replay.regen import regen_segment, DummyFrameReader
|
||||||
from openpilot.selfdrive.test.process_replay.process_replay import check_openpilot_enabled
|
from openpilot.selfdrive.test.process_replay.process_replay import check_openpilot_enabled
|
||||||
from openpilot.tools.lib.openpilotci import get_url
|
from openpilot.tools.lib.openpilotci import get_url
|
||||||
from openpilot.tools.lib.logreader import LogReader
|
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"))
|
lr = LogReader(get_url(route, sidx, "rlog.bz2"))
|
||||||
frs = {
|
frs = {
|
||||||
'roadCameraState': FrameReader(get_url(route, sidx, "fcamera.hevc")),
|
'roadCameraState': FrameReader(get_url(route, sidx, "fcamera.hevc")),
|
||||||
'driverCameraState': FrameReader(get_url(route, sidx, "fcamera.hevc")),
|
'driverCameraState': DummyFrameReader.zero_dcamera()
|
||||||
}
|
}
|
||||||
if next((True for m in lr if m.which() == "wideRoadCameraState"), False):
|
if next((True for m in lr if m.which() == "wideRoadCameraState"), False):
|
||||||
frs["wideRoadCameraState"] = FrameReader(get_url(route, sidx, "ecamera.hevc"))
|
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)
|
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)
|
f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter], LIBS=raylib_libs)
|
||||||
# keep installers small
|
# keep installers small
|
||||||
assert f[0].get_size() < 1900*1e3, f[0].get_size()
|
assert f[0].get_size() < 1300*1e3, f[0].get_size()
|
||||||
|
|
||||||
# build watch3
|
# build watch3
|
||||||
if arch in ['x86_64', 'aarch64', 'Darwin'] or GetOption('extras'):
|
if arch in ['x86_64', 'aarch64', 'Darwin'] or GetOption('extras'):
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ from collections.abc import Callable
|
|||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.selfdrive.ui.widgets.offroad_alerts import UpdateAlert, OffroadAlert
|
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.text_measure import measure_text_cached
|
||||||
|
from openpilot.system.ui.lib.label import gui_label
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR
|
||||||
from openpilot.system.ui.lib.widget import Widget
|
|
||||||
|
|
||||||
HEADER_HEIGHT = 80
|
HEADER_HEIGHT = 80
|
||||||
HEAD_BUTTON_FONT_SIZE = 40
|
HEAD_BUTTON_FONT_SIZE = 40
|
||||||
@@ -27,9 +25,8 @@ class HomeLayoutState(IntEnum):
|
|||||||
ALERTS = 2
|
ALERTS = 2
|
||||||
|
|
||||||
|
|
||||||
class HomeLayout(Widget):
|
class HomeLayout:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
|
||||||
self.params = Params()
|
self.params = Params()
|
||||||
|
|
||||||
self.update_alert = UpdateAlert()
|
self.update_alert = UpdateAlert()
|
||||||
@@ -50,10 +47,6 @@ class HomeLayout(Widget):
|
|||||||
self.update_notif_rect = rl.Rectangle(0, 0, 200, HEADER_HEIGHT - 10)
|
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.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()
|
self._setup_callbacks()
|
||||||
|
|
||||||
def _setup_callbacks(self):
|
def _setup_callbacks(self):
|
||||||
@@ -66,7 +59,9 @@ class HomeLayout(Widget):
|
|||||||
def _set_state(self, state: HomeLayoutState):
|
def _set_state(self, state: HomeLayoutState):
|
||||||
self.current_state = state
|
self.current_state = state
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle):
|
def render(self, rect: rl.Rectangle):
|
||||||
|
self._update_layout_rects(rect)
|
||||||
|
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
if current_time - self.last_refresh >= REFRESH_INTERVAL:
|
if current_time - self.last_refresh >= REFRESH_INTERVAL:
|
||||||
self._refresh()
|
self._refresh()
|
||||||
@@ -83,16 +78,16 @@ class HomeLayout(Widget):
|
|||||||
elif self.current_state == HomeLayoutState.ALERTS:
|
elif self.current_state == HomeLayoutState.ALERTS:
|
||||||
self._render_alerts_view()
|
self._render_alerts_view()
|
||||||
|
|
||||||
def _update_layout_rects(self):
|
def _update_layout_rects(self, rect: rl.Rectangle):
|
||||||
self.header_rect = rl.Rectangle(
|
self.header_rect = rl.Rectangle(
|
||||||
self._rect.x + CONTENT_MARGIN, self._rect.y + CONTENT_MARGIN, self._rect.width - 2 * CONTENT_MARGIN, HEADER_HEIGHT
|
rect.x + CONTENT_MARGIN, rect.y + CONTENT_MARGIN, rect.width - 2 * CONTENT_MARGIN, HEADER_HEIGHT
|
||||||
)
|
)
|
||||||
|
|
||||||
content_y = self._rect.y + CONTENT_MARGIN + HEADER_HEIGHT + SPACING
|
content_y = rect.y + CONTENT_MARGIN + HEADER_HEIGHT + SPACING
|
||||||
content_height = self._rect.height - CONTENT_MARGIN - HEADER_HEIGHT - SPACING - CONTENT_MARGIN
|
content_height = rect.height - CONTENT_MARGIN - HEADER_HEIGHT - SPACING - CONTENT_MARGIN
|
||||||
|
|
||||||
self.content_rect = rl.Rectangle(
|
self.content_rect = rl.Rectangle(
|
||||||
self._rect.x + CONTENT_MARGIN, content_y, self._rect.width - 2 * CONTENT_MARGIN, content_height
|
rect.x + CONTENT_MARGIN, content_y, rect.width - 2 * CONTENT_MARGIN, content_height
|
||||||
)
|
)
|
||||||
|
|
||||||
left_width = self.content_rect.width - RIGHT_COLUMN_WIDTH - SPACING
|
left_width = self.content_rect.width - RIGHT_COLUMN_WIDTH - SPACING
|
||||||
@@ -175,25 +170,28 @@ class HomeLayout(Widget):
|
|||||||
self.offroad_alert.render(self.content_rect)
|
self.offroad_alert.render(self.content_rect)
|
||||||
|
|
||||||
def _render_left_column(self):
|
def _render_left_column(self):
|
||||||
self._prime_widget.render(self.left_column_rect)
|
rl.draw_rectangle_rounded(self.left_column_rect, 0.02, 10, PRIME_BG_COLOR)
|
||||||
|
gui_label(self.left_column_rect, "Prime Widget", 48, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||||
|
|
||||||
def _render_right_column(self):
|
def _render_right_column(self):
|
||||||
exp_height = 125
|
widget_height = (self.right_column_rect.height - SPACING) // 2
|
||||||
|
|
||||||
exp_rect = rl.Rectangle(
|
exp_rect = rl.Rectangle(
|
||||||
self.right_column_rect.x, self.right_column_rect.y, self.right_column_rect.width, exp_height
|
self.right_column_rect.x, self.right_column_rect.y, self.right_column_rect.width, widget_height
|
||||||
)
|
)
|
||||||
self._exp_mode_button.render(exp_rect)
|
rl.draw_rectangle_rounded(exp_rect, 0.02, 10, PRIME_BG_COLOR)
|
||||||
|
gui_label(exp_rect, "Experimental Mode", 36, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||||
|
|
||||||
setup_rect = rl.Rectangle(
|
setup_rect = rl.Rectangle(
|
||||||
self.right_column_rect.x,
|
self.right_column_rect.x,
|
||||||
self.right_column_rect.y + exp_height + SPACING,
|
self.right_column_rect.y + widget_height + SPACING,
|
||||||
self.right_column_rect.width,
|
self.right_column_rect.width,
|
||||||
self.right_column_rect.height - exp_height - SPACING,
|
widget_height,
|
||||||
)
|
)
|
||||||
self._setup_widget.render(setup_rect)
|
rl.draw_rectangle_rounded(setup_rect, 0.02, 10, PRIME_BG_COLOR)
|
||||||
|
gui_label(setup_rect, "Setup", 36, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||||
|
|
||||||
def _refresh(self):
|
def _refresh(self):
|
||||||
# TODO: implement _update_state with a timer
|
|
||||||
self.update_available = self.update_alert.refresh()
|
self.update_available = self.update_alert.refresh()
|
||||||
self.alert_count = self.offroad_alert.refresh()
|
self.alert_count = self.offroad_alert.refresh()
|
||||||
self._update_state_priority(self.update_available, self.alert_count > 0)
|
self._update_state_priority(self.update_available, self.alert_count > 0)
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import pyray as rl
|
import pyray as rl
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
import cereal.messaging as messaging
|
|
||||||
from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH
|
from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH
|
||||||
from openpilot.selfdrive.ui.layouts.home import HomeLayout
|
from openpilot.selfdrive.ui.layouts.home import HomeLayout
|
||||||
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType
|
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout
|
||||||
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
|
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
|
||||||
from openpilot.system.ui.lib.widget import Widget
|
|
||||||
|
|
||||||
|
|
||||||
class MainState(IntEnum):
|
class MainState(IntEnum):
|
||||||
@@ -15,15 +13,14 @@ class MainState(IntEnum):
|
|||||||
ONROAD = 2
|
ONROAD = 2
|
||||||
|
|
||||||
|
|
||||||
class MainLayout(Widget):
|
class MainLayout:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self._pm = messaging.PubMaster(['userFlag'])
|
|
||||||
|
|
||||||
self._sidebar = Sidebar()
|
self._sidebar = Sidebar()
|
||||||
|
self._sidebar_visible = True
|
||||||
self._current_mode = MainState.HOME
|
self._current_mode = MainState.HOME
|
||||||
self._prev_onroad = False
|
self._prev_onroad = False
|
||||||
|
self._window_rect = None
|
||||||
|
self._current_callback: callable | None = None
|
||||||
|
|
||||||
# Initialize layouts
|
# Initialize layouts
|
||||||
self._layouts = {MainState.HOME: HomeLayout(), MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()}
|
self._layouts = {MainState.HOME: HomeLayout(), MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()}
|
||||||
@@ -34,23 +31,32 @@ class MainLayout(Widget):
|
|||||||
# Set callbacks
|
# Set callbacks
|
||||||
self._setup_callbacks()
|
self._setup_callbacks()
|
||||||
|
|
||||||
def _render(self, _):
|
def render(self, rect):
|
||||||
|
self._current_callback = None
|
||||||
|
|
||||||
|
self._update_layout_rects(rect)
|
||||||
self._handle_onroad_transition()
|
self._handle_onroad_transition()
|
||||||
self._render_main_content()
|
self._render_main_content()
|
||||||
|
self._handle_input()
|
||||||
|
|
||||||
|
if self._current_callback:
|
||||||
|
self._current_callback()
|
||||||
|
|
||||||
def _setup_callbacks(self):
|
def _setup_callbacks(self):
|
||||||
self._sidebar.set_callbacks(on_settings=self._on_settings_clicked,
|
self._sidebar.set_callbacks(
|
||||||
on_flag=self._on_flag_clicked)
|
on_settings=lambda: setattr(self, '_current_callback', self._on_settings_clicked),
|
||||||
self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE))
|
on_flag=lambda: setattr(self, '_current_callback', self._on_flag_clicked),
|
||||||
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)
|
self._layouts[MainState.SETTINGS].set_callbacks(
|
||||||
device.add_interactive_timeout_callback(self._set_mode_for_state)
|
on_close=lambda: setattr(self, '_current_callback', self._set_mode_for_state)
|
||||||
|
)
|
||||||
|
|
||||||
def _update_layout_rects(self):
|
def _update_layout_rects(self, rect):
|
||||||
self._sidebar_rect = rl.Rectangle(self._rect.x, self._rect.y, SIDEBAR_WIDTH, self._rect.height)
|
self._window_rect = rect
|
||||||
|
self._sidebar_rect = rl.Rectangle(rect.x, rect.y, SIDEBAR_WIDTH, rect.height)
|
||||||
|
|
||||||
x_offset = SIDEBAR_WIDTH if self._sidebar.is_visible else 0
|
x_offset = SIDEBAR_WIDTH if self._sidebar_visible else 0
|
||||||
self._content_rect = rl.Rectangle(self._rect.y + x_offset, self._rect.y, self._rect.width - x_offset, self._rect.height)
|
self._content_rect = rl.Rectangle(rect.y + x_offset, rect.y, rect.width - x_offset, rect.height)
|
||||||
|
|
||||||
def _handle_onroad_transition(self):
|
def _handle_onroad_transition(self):
|
||||||
if ui_state.started != self._prev_onroad:
|
if ui_state.started != self._prev_onroad:
|
||||||
@@ -60,34 +66,31 @@ class MainLayout(Widget):
|
|||||||
|
|
||||||
def _set_mode_for_state(self):
|
def _set_mode_for_state(self):
|
||||||
if ui_state.started:
|
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
|
self._current_mode = MainState.ONROAD
|
||||||
|
self._sidebar_visible = False
|
||||||
else:
|
else:
|
||||||
self._current_mode = MainState.HOME
|
self._current_mode = MainState.HOME
|
||||||
self._sidebar.set_visible(True)
|
self._sidebar_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):
|
def _on_settings_clicked(self):
|
||||||
self.open_settings(PanelType.DEVICE)
|
self._current_mode = MainState.SETTINGS
|
||||||
|
self._sidebar_visible = False
|
||||||
|
|
||||||
def _on_flag_clicked(self):
|
def _on_flag_clicked(self):
|
||||||
user_flag = messaging.new_message('userFlag')
|
pass
|
||||||
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):
|
def _render_main_content(self):
|
||||||
# Render sidebar
|
# Render sidebar
|
||||||
if self._sidebar.is_visible:
|
if self._sidebar_visible:
|
||||||
self._sidebar.render(self._sidebar_rect)
|
self._sidebar.render(self._sidebar_rect)
|
||||||
|
|
||||||
content_rect = self._content_rect if self._sidebar.is_visible else self._rect
|
content_rect = self._content_rect if self._sidebar_visible else self._window_rect
|
||||||
self._layouts[self._current_mode].render(content_rect)
|
self._layouts[self._current_mode].render(content_rect)
|
||||||
|
|
||||||
|
def _handle_input(self):
|
||||||
|
if self._current_mode != MainState.ONROAD or not rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||||
|
return
|
||||||
|
|
||||||
|
mouse_pos = rl.get_mouse_position()
|
||||||
|
if rl.check_collision_point_rec(mouse_pos, self._content_rect):
|
||||||
|
self._sidebar_visible = not self._sidebar_visible
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import pyray as rl
|
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.lib.wifi_manager import WifiManagerWrapper
|
||||||
from openpilot.system.ui.widgets.network import WifiManagerUI
|
from openpilot.system.ui.widgets.network import WifiManagerUI
|
||||||
|
|
||||||
|
|
||||||
class NetworkLayout(Widget):
|
class NetworkLayout:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
|
||||||
self.wifi_manager = WifiManagerWrapper()
|
self.wifi_manager = WifiManagerWrapper()
|
||||||
self.wifi_ui = WifiManagerUI(self.wifi_manager)
|
self.wifi_ui = WifiManagerUI(self.wifi_manager)
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle):
|
def render(self, rect: rl.Rectangle):
|
||||||
self.wifi_ui.render(rect)
|
self.wifi_ui.render(rect)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def require_full_screen(self):
|
||||||
|
return self.wifi_ui.require_full_screen
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.wifi_manager.shutdown()
|
self.wifi_manager.shutdown()
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
from openpilot.system.ui.lib.list_view import toggle_item
|
from openpilot.system.ui.lib.list_view import ListView, 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.common.params import Params
|
||||||
from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item
|
|
||||||
|
|
||||||
# Description constants
|
# Description constants
|
||||||
DESCRIPTIONS = {
|
DESCRIPTIONS = {
|
||||||
@@ -11,16 +8,11 @@ DESCRIPTIONS = {
|
|||||||
"See https://docs.comma.ai/how-to/connect-to-comma for more info."
|
"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)",
|
'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):
|
class DeveloperLayout:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
|
||||||
self._params = Params()
|
self._params = Params()
|
||||||
items = [
|
items = [
|
||||||
toggle_item(
|
toggle_item(
|
||||||
@@ -29,7 +21,6 @@ class DeveloperLayout(Widget):
|
|||||||
initial_state=self._params.get_bool("AdbEnabled"),
|
initial_state=self._params.get_bool("AdbEnabled"),
|
||||||
callback=self._on_enable_adb,
|
callback=self._on_enable_adb,
|
||||||
),
|
),
|
||||||
ssh_key_item("SSH Key", description=DESCRIPTIONS["ssh_key"]),
|
|
||||||
toggle_item(
|
toggle_item(
|
||||||
"Joystick Debug Mode",
|
"Joystick Debug Mode",
|
||||||
description=DESCRIPTIONS["joystick_debug_mode"],
|
description=DESCRIPTIONS["joystick_debug_mode"],
|
||||||
@@ -50,10 +41,10 @@ class DeveloperLayout(Widget):
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
self._list_widget = ListView(items)
|
||||||
|
|
||||||
def _render(self, rect):
|
def render(self, rect):
|
||||||
self._scroller.render(rect)
|
self._list_widget.render(rect)
|
||||||
|
|
||||||
def _on_enable_adb(self): pass
|
def _on_enable_adb(self): pass
|
||||||
def _on_joystick_debug_mode(self): pass
|
def _on_joystick_debug_mode(self): pass
|
||||||
|
|||||||
@@ -1,150 +1,47 @@
|
|||||||
import os
|
from openpilot.system.ui.lib.list_view import ListView, text_item, button_item
|
||||||
import json
|
|
||||||
|
|
||||||
from openpilot.common.basedir import BASEDIR
|
|
||||||
from openpilot.common.params import Params
|
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.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
|
# Description constants
|
||||||
DESCRIPTIONS = {
|
DESCRIPTIONS = {
|
||||||
'pair_device': "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.",
|
'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)",
|
'driver_camera': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
|
||||||
'reset_calibration': (
|
'reset_calibration': (
|
||||||
"openpilot requires the device to be mounted within 4° left or right and within 5° " +
|
"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."
|
"up or 9° down. openpilot is continuously calibrating, resetting is rarely required."
|
||||||
),
|
),
|
||||||
'review_guide': "Review the rules, features, and limitations of openpilot",
|
'review_guide': "Review the rules, features, and limitations of openpilot",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceLayout(Widget):
|
class DeviceLayout:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
params = Params()
|
||||||
|
dongle_id = params.get("DongleId", encoding="utf-8") or "N/A"
|
||||||
self._params = Params()
|
serial = params.get("HardwareSerial") or "N/A"
|
||||||
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 = [
|
items = [
|
||||||
text_item("Dongle ID", dongle_id),
|
text_item("Dongle ID", dongle_id),
|
||||||
text_item("Serial", serial),
|
text_item("Serial", serial),
|
||||||
button_item("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], callback=self._pair_device),
|
button_item("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], self._on_pair_device),
|
||||||
button_item("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], callback=self._show_driver_camera, enabled=ui_state.is_offroad),
|
button_item("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], self._on_driver_camera),
|
||||||
button_item("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt),
|
button_item("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], self._on_reset_calibration),
|
||||||
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("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):
|
if TICI:
|
||||||
self._scroller.render(rect)
|
items.append(button_item("Regulatory", "VIEW", callback=self._on_regulatory))
|
||||||
|
|
||||||
def _show_language_selection(self):
|
items.append(button_item("Change Language", "CHANGE", callback=self._on_change_language))
|
||||||
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)
|
self._list_widget = ListView(items)
|
||||||
gui_app.set_modal_overlay(self._select_language_dialog, callback=self._handle_language_selection)
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _handle_language_selection(self, result: int):
|
def render(self, rect):
|
||||||
if result == 1 and self._select_language_dialog:
|
self._list_widget.render(rect)
|
||||||
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_pair_device(self): pass
|
||||||
|
def _on_driver_camera(self): pass
|
||||||
|
def _on_reset_calibration(self): pass
|
||||||
def _on_review_training_guide(self): pass
|
def _on_review_training_guide(self): pass
|
||||||
|
def _on_regulatory(self): pass
|
||||||
|
def _on_change_language(self): pass
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -2,15 +2,15 @@ import pyray as rl
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from openpilot.common.params import Params
|
||||||
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
|
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
|
||||||
from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
|
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.software import SoftwareLayout
|
||||||
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
|
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
|
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.text_measure import measure_text_cached
|
||||||
from openpilot.selfdrive.ui.layouts.network import NetworkLayout
|
from openpilot.selfdrive.ui.layouts.network import NetworkLayout
|
||||||
from openpilot.system.ui.lib.widget import Widget
|
|
||||||
|
|
||||||
# Import individual panels
|
# Import individual panels
|
||||||
|
|
||||||
@@ -18,8 +18,9 @@ SETTINGS_CLOSE_TEXT = "X"
|
|||||||
# Constants
|
# Constants
|
||||||
SIDEBAR_WIDTH = 500
|
SIDEBAR_WIDTH = 500
|
||||||
CLOSE_BTN_SIZE = 200
|
CLOSE_BTN_SIZE = 200
|
||||||
NAV_BTN_HEIGHT = 110
|
NAV_BTN_HEIGHT = 80
|
||||||
PANEL_MARGIN = 50
|
PANEL_MARGIN = 50
|
||||||
|
SCROLL_SPEED = 30
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
SIDEBAR_COLOR = rl.BLACK
|
SIDEBAR_COLOR = rl.BLACK
|
||||||
@@ -28,6 +29,7 @@ CLOSE_BTN_COLOR = rl.Color(41, 41, 41, 255)
|
|||||||
CLOSE_BTN_PRESSED = rl.Color(59, 59, 59, 255)
|
CLOSE_BTN_PRESSED = rl.Color(59, 59, 59, 255)
|
||||||
TEXT_NORMAL = rl.Color(128, 128, 128, 255)
|
TEXT_NORMAL = rl.Color(128, 128, 128, 255)
|
||||||
TEXT_SELECTED = rl.Color(255, 255, 255, 255)
|
TEXT_SELECTED = rl.Color(255, 255, 255, 255)
|
||||||
|
TEXT_PRESSED = rl.Color(173, 173, 173, 255)
|
||||||
|
|
||||||
|
|
||||||
class PanelType(IntEnum):
|
class PanelType(IntEnum):
|
||||||
@@ -43,22 +45,23 @@ class PanelType(IntEnum):
|
|||||||
class PanelInfo:
|
class PanelInfo:
|
||||||
name: str
|
name: str
|
||||||
instance: object
|
instance: object
|
||||||
button_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
|
button_rect: rl.Rectangle
|
||||||
|
|
||||||
|
|
||||||
class SettingsLayout(Widget):
|
class SettingsLayout:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
self._params = Params()
|
||||||
self._current_panel = PanelType.DEVICE
|
self._current_panel = PanelType.DEVICE
|
||||||
|
self._max_scroll = 0.0
|
||||||
|
|
||||||
# Panel configuration
|
# Panel configuration
|
||||||
self._panels = {
|
self._panels = {
|
||||||
PanelType.DEVICE: PanelInfo("Device", DeviceLayout()),
|
PanelType.DEVICE: PanelInfo("Device", DeviceLayout(), rl.Rectangle(0, 0, 0, 0)),
|
||||||
PanelType.NETWORK: PanelInfo("Network", NetworkLayout()),
|
PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout(), rl.Rectangle(0, 0, 0, 0)),
|
||||||
PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout()),
|
PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout(), rl.Rectangle(0, 0, 0, 0)),
|
||||||
PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout()),
|
PanelType.FIREHOSE: PanelInfo("Firehose", None, rl.Rectangle(0, 0, 0, 0)),
|
||||||
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout()),
|
PanelType.NETWORK: PanelInfo("Network", NetworkLayout(), rl.Rectangle(0, 0, 0, 0)),
|
||||||
PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayout()),
|
PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayout(), rl.Rectangle(0, 0, 0, 0)),
|
||||||
}
|
}
|
||||||
|
|
||||||
self._font_medium = gui_app.font(FontWeight.MEDIUM)
|
self._font_medium = gui_app.font(FontWeight.MEDIUM)
|
||||||
@@ -70,7 +73,7 @@ class SettingsLayout(Widget):
|
|||||||
def set_callbacks(self, on_close: Callable):
|
def set_callbacks(self, on_close: Callable):
|
||||||
self._close_callback = on_close
|
self._close_callback = on_close
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle):
|
def render(self, rect: rl.Rectangle):
|
||||||
# Calculate layout
|
# Calculate layout
|
||||||
sidebar_rect = rl.Rectangle(rect.x, rect.y, SIDEBAR_WIDTH, rect.height)
|
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)
|
panel_rect = rl.Rectangle(rect.x + SIDEBAR_WIDTH, rect.y, rect.width - SIDEBAR_WIDTH, rect.height)
|
||||||
@@ -79,6 +82,9 @@ class SettingsLayout(Widget):
|
|||||||
self._draw_sidebar(sidebar_rect)
|
self._draw_sidebar(sidebar_rect)
|
||||||
self._draw_current_panel(panel_rect)
|
self._draw_current_panel(panel_rect)
|
||||||
|
|
||||||
|
if rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||||
|
self.handle_mouse_release(rl.get_mouse_position())
|
||||||
|
|
||||||
def _draw_sidebar(self, rect: rl.Rectangle):
|
def _draw_sidebar(self, rect: rl.Rectangle):
|
||||||
rl.draw_rectangle_rec(rect, SIDEBAR_COLOR)
|
rl.draw_rectangle_rec(rect, SIDEBAR_COLOR)
|
||||||
|
|
||||||
@@ -103,9 +109,17 @@ class SettingsLayout(Widget):
|
|||||||
self._close_btn_rect = close_btn_rect
|
self._close_btn_rect = close_btn_rect
|
||||||
|
|
||||||
# Navigation buttons
|
# Navigation buttons
|
||||||
y = rect.y + 300
|
nav_start_y = rect.y + 300
|
||||||
|
button_spacing = 20
|
||||||
|
|
||||||
|
i = 0
|
||||||
for panel_type, panel_info in self._panels.items():
|
for panel_type, panel_info in self._panels.items():
|
||||||
button_rect = rl.Rectangle(rect.x + 50, y, rect.width - 150, NAV_BTN_HEIGHT)
|
button_rect = rl.Rectangle(
|
||||||
|
rect.x + 50,
|
||||||
|
nav_start_y + i * (NAV_BTN_HEIGHT + button_spacing),
|
||||||
|
rect.width - 150, # Right-aligned with margin
|
||||||
|
NAV_BTN_HEIGHT,
|
||||||
|
)
|
||||||
|
|
||||||
# Button styling
|
# Button styling
|
||||||
is_selected = panel_type == self._current_panel
|
is_selected = panel_type == self._current_panel
|
||||||
@@ -119,8 +133,7 @@ class SettingsLayout(Widget):
|
|||||||
|
|
||||||
# Store button rect for click detection
|
# Store button rect for click detection
|
||||||
panel_info.button_rect = button_rect
|
panel_info.button_rect = button_rect
|
||||||
|
i += 1
|
||||||
y += NAV_BTN_HEIGHT
|
|
||||||
|
|
||||||
def _draw_current_panel(self, rect: rl.Rectangle):
|
def _draw_current_panel(self, rect: rl.Rectangle):
|
||||||
rl.draw_rectangle_rounded(
|
rl.draw_rectangle_rounded(
|
||||||
@@ -131,8 +144,17 @@ class SettingsLayout(Widget):
|
|||||||
panel = self._panels[self._current_panel]
|
panel = self._panels[self._current_panel]
|
||||||
if panel.instance:
|
if panel.instance:
|
||||||
panel.instance.render(content_rect)
|
panel.instance.render(content_rect)
|
||||||
|
else:
|
||||||
|
gui_text_box(
|
||||||
|
content_rect,
|
||||||
|
f"Demo {self._panels[self._current_panel].name} Panel",
|
||||||
|
font_size=170,
|
||||||
|
color=rl.WHITE,
|
||||||
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
||||||
|
)
|
||||||
|
|
||||||
def _handle_mouse_release(self, mouse_pos: rl.Vector2) -> bool:
|
def handle_mouse_release(self, mouse_pos: rl.Vector2) -> bool:
|
||||||
# Check close button
|
# Check close button
|
||||||
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
|
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
|
||||||
if self._close_callback:
|
if self._close_callback:
|
||||||
@@ -142,15 +164,20 @@ class SettingsLayout(Widget):
|
|||||||
# Check navigation buttons
|
# Check navigation buttons
|
||||||
for panel_type, panel_info in self._panels.items():
|
for panel_type, panel_info in self._panels.items():
|
||||||
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect):
|
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect):
|
||||||
self.set_current_panel(panel_type)
|
self._switch_to_panel(panel_type)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def set_current_panel(self, panel_type: PanelType):
|
def _switch_to_panel(self, panel_type: PanelType):
|
||||||
if panel_type != self._current_panel:
|
if panel_type != self._current_panel:
|
||||||
self._current_panel = panel_type
|
self._current_panel = panel_type
|
||||||
|
|
||||||
|
def set_current_panel(self, index: int, param: str = ""):
|
||||||
|
panel_types = list(self._panels.keys())
|
||||||
|
if 0 <= index < len(panel_types):
|
||||||
|
self._switch_to_panel(panel_types[index])
|
||||||
|
|
||||||
def close_settings(self):
|
def close_settings(self):
|
||||||
if self._close_callback:
|
if self._close_callback:
|
||||||
self._close_callback()
|
self._close_callback()
|
||||||
|
|||||||
@@ -1,20 +1,7 @@
|
|||||||
from openpilot.common.params import Params
|
from openpilot.system.ui.lib.list_view import ListView, button_item, text_item
|
||||||
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:
|
||||||
class SoftwareLayout(Widget):
|
|
||||||
def __init__(self):
|
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 = [
|
items = [
|
||||||
text_item("Current Version", ""),
|
text_item("Current Version", ""),
|
||||||
button_item("Download", "CHECK", callback=self._on_download_update),
|
button_item("Download", "CHECK", callback=self._on_download_update),
|
||||||
@@ -22,21 +9,13 @@ class SoftwareLayout(Widget):
|
|||||||
button_item("Target Branch", "SELECT", callback=self._on_select_branch),
|
button_item("Target Branch", "SELECT", callback=self._on_select_branch),
|
||||||
button_item("Uninstall", "UNINSTALL", callback=self._on_uninstall),
|
button_item("Uninstall", "UNINSTALL", callback=self._on_uninstall),
|
||||||
]
|
]
|
||||||
return items
|
|
||||||
|
|
||||||
def _render(self, rect):
|
self._list_widget = ListView(items)
|
||||||
self._scroller.render(rect)
|
|
||||||
|
def render(self, rect):
|
||||||
|
self._list_widget.render(rect)
|
||||||
|
|
||||||
def _on_download_update(self): pass
|
def _on_download_update(self): pass
|
||||||
def _on_install_update(self): pass
|
def _on_install_update(self): pass
|
||||||
def _on_select_branch(self): pass
|
def _on_select_branch(self): pass
|
||||||
|
def _on_uninstall(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,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
from openpilot.system.ui.lib.list_view import multiple_button_item, toggle_item
|
from openpilot.system.ui.lib.list_view import ListView, 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.common.params import Params
|
||||||
|
|
||||||
# Description constants
|
# Description constants
|
||||||
@@ -10,14 +8,9 @@ DESCRIPTIONS = {
|
|||||||
"Your attention is required at all times to use this feature."
|
"Your attention is required at all times to use this feature."
|
||||||
),
|
),
|
||||||
"DisengageOnAccelerator": "When enabled, pressing the accelerator pedal will disengage openpilot.",
|
"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": (
|
"IsLdwEnabled": (
|
||||||
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
|
"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)."
|
"without a turn signal activated while driving over 31 mph (50 km/h)."
|
||||||
),
|
),
|
||||||
"AlwaysOnDM": "Enable driver monitoring even when openpilot is not engaged.",
|
"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.",
|
'RecordFront': "Upload data from the driver facing camera and help improve the driver monitoring algorithm.",
|
||||||
@@ -25,9 +18,8 @@ DESCRIPTIONS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TogglesLayout(Widget):
|
class TogglesLayout:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
|
||||||
self._params = Params()
|
self._params = Params()
|
||||||
items = [
|
items = [
|
||||||
toggle_item(
|
toggle_item(
|
||||||
@@ -47,15 +39,6 @@ class TogglesLayout(Widget):
|
|||||||
self._params.get_bool("DisengageOnAccelerator"),
|
self._params.get_bool("DisengageOnAccelerator"),
|
||||||
icon="disengage_on_accelerator.png",
|
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(
|
toggle_item(
|
||||||
"Enable Lane Departure Warnings",
|
"Enable Lane Departure Warnings",
|
||||||
DESCRIPTIONS["IsLdwEnabled"],
|
DESCRIPTIONS["IsLdwEnabled"],
|
||||||
@@ -79,10 +62,7 @@ class TogglesLayout(Widget):
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
self._list_widget = ListView(items)
|
||||||
|
|
||||||
def _render(self, rect):
|
def render(self, rect):
|
||||||
self._scroller.render(rect)
|
self._list_widget.render(rect)
|
||||||
|
|
||||||
def _set_longitudinal_personality(self, button_index: int):
|
|
||||||
self._params.put("LongitudinalPersonality", str(button_index))
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from cereal import log
|
|||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
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.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.lib.widget import Widget
|
|
||||||
|
|
||||||
SIDEBAR_WIDTH = 300
|
SIDEBAR_WIDTH = 300
|
||||||
METRIC_HEIGHT = 126
|
METRIC_HEIGHT = 126
|
||||||
@@ -19,7 +18,6 @@ HOME_BTN = rl.Rectangle(60, 860, 180, 180)
|
|||||||
ThermalStatus = log.DeviceState.ThermalStatus
|
ThermalStatus = log.DeviceState.ThermalStatus
|
||||||
NetworkType = log.DeviceState.NetworkType
|
NetworkType = log.DeviceState.NetworkType
|
||||||
|
|
||||||
|
|
||||||
# Color scheme
|
# Color scheme
|
||||||
class Colors:
|
class Colors:
|
||||||
SIDEBAR_BG = rl.Color(57, 57, 57, 255)
|
SIDEBAR_BG = rl.Color(57, 57, 57, 255)
|
||||||
@@ -37,7 +35,6 @@ class Colors:
|
|||||||
BUTTON_NORMAL = rl.Color(255, 255, 255, 255)
|
BUTTON_NORMAL = rl.Color(255, 255, 255, 255)
|
||||||
BUTTON_PRESSED = rl.Color(255, 255, 255, 166)
|
BUTTON_PRESSED = rl.Color(255, 255, 255, 166)
|
||||||
|
|
||||||
|
|
||||||
NETWORK_TYPES = {
|
NETWORK_TYPES = {
|
||||||
NetworkType.none: "Offline",
|
NetworkType.none: "Offline",
|
||||||
NetworkType.wifi: "WiFi",
|
NetworkType.wifi: "WiFi",
|
||||||
@@ -60,10 +57,8 @@ class MetricData:
|
|||||||
self.value = value
|
self.value = value
|
||||||
self.color = color
|
self.color = color
|
||||||
|
|
||||||
|
class Sidebar:
|
||||||
class Sidebar(Widget):
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
|
||||||
self._net_type = NETWORK_TYPES.get(NetworkType.none)
|
self._net_type = NETWORK_TYPES.get(NetworkType.none)
|
||||||
self._net_strength = 0
|
self._net_strength = 0
|
||||||
|
|
||||||
@@ -77,7 +72,7 @@ class Sidebar(Widget):
|
|||||||
self._font_regular = gui_app.font(FontWeight.NORMAL)
|
self._font_regular = gui_app.font(FontWeight.NORMAL)
|
||||||
self._font_bold = gui_app.font(FontWeight.SEMI_BOLD)
|
self._font_bold = gui_app.font(FontWeight.SEMI_BOLD)
|
||||||
|
|
||||||
# Callbacks
|
# Callbacks
|
||||||
self._on_settings_click: Callable | None = None
|
self._on_settings_click: Callable | None = None
|
||||||
self._on_flag_click: Callable | None = None
|
self._on_flag_click: Callable | None = None
|
||||||
|
|
||||||
@@ -85,7 +80,9 @@ class Sidebar(Widget):
|
|||||||
self._on_settings_click = on_settings
|
self._on_settings_click = on_settings
|
||||||
self._on_flag_click = on_flag
|
self._on_flag_click = on_flag
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle):
|
def render(self, rect: rl.Rectangle):
|
||||||
|
self.update_state()
|
||||||
|
|
||||||
# Background
|
# Background
|
||||||
rl.draw_rectangle_rec(rect, Colors.SIDEBAR_BG)
|
rl.draw_rectangle_rec(rect, Colors.SIDEBAR_BG)
|
||||||
|
|
||||||
@@ -93,7 +90,9 @@ class Sidebar(Widget):
|
|||||||
self._draw_network_indicator(rect)
|
self._draw_network_indicator(rect)
|
||||||
self._draw_metrics(rect)
|
self._draw_metrics(rect)
|
||||||
|
|
||||||
def _update_state(self):
|
self._handle_mouse_release()
|
||||||
|
|
||||||
|
def update_state(self):
|
||||||
sm = ui_state.sm
|
sm = ui_state.sm
|
||||||
if not sm.updated['deviceState']:
|
if not sm.updated['deviceState']:
|
||||||
return
|
return
|
||||||
@@ -135,7 +134,11 @@ class Sidebar(Widget):
|
|||||||
else:
|
else:
|
||||||
self._panda_status.update("VEHICLE", "ONLINE", Colors.GOOD)
|
self._panda_status.update("VEHICLE", "ONLINE", Colors.GOOD)
|
||||||
|
|
||||||
def _handle_mouse_release(self, mouse_pos: rl.Vector2):
|
def _handle_mouse_release(self):
|
||||||
|
if not rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||||
|
return
|
||||||
|
|
||||||
|
mouse_pos = rl.get_mouse_position()
|
||||||
if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN):
|
if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN):
|
||||||
if self._on_settings_click:
|
if self._on_settings_click:
|
||||||
self._on_settings_click()
|
self._on_settings_click()
|
||||||
@@ -145,7 +148,8 @@ class Sidebar(Widget):
|
|||||||
|
|
||||||
def _draw_buttons(self, rect: rl.Rectangle):
|
def _draw_buttons(self, rect: rl.Rectangle):
|
||||||
mouse_pos = rl.get_mouse_position()
|
mouse_pos = rl.get_mouse_position()
|
||||||
mouse_down = self._is_pressed and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
||||||
|
|
||||||
|
|
||||||
# Settings button
|
# Settings button
|
||||||
settings_down = mouse_down and rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN)
|
settings_down = mouse_down and rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN)
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -6,9 +6,9 @@ from openpilot.system.hardware import TICI
|
|||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_FPS
|
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.label import gui_text_box
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
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
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
|
|
||||||
|
|
||||||
ALERT_MARGIN = 40
|
ALERT_MARGIN = 40
|
||||||
ALERT_PADDING = 60
|
ALERT_PADDING = 60
|
||||||
ALERT_LINE_SPACING = 45
|
ALERT_LINE_SPACING = 45
|
||||||
@@ -21,6 +21,7 @@ ALERT_FONT_BIG = 88
|
|||||||
SELFDRIVE_STATE_TIMEOUT = 5 # Seconds
|
SELFDRIVE_STATE_TIMEOUT = 5 # Seconds
|
||||||
SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 # Seconds
|
SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 # Seconds
|
||||||
|
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
ALERT_COLORS = {
|
ALERT_COLORS = {
|
||||||
log.SelfdriveState.AlertStatus.normal: rl.Color(0, 0, 0, 235), # Black
|
log.SelfdriveState.AlertStatus.normal: rl.Color(0, 0, 0, 235), # Black
|
||||||
@@ -60,9 +61,8 @@ ALERT_CRITICAL_REBOOT = Alert(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AlertRenderer(Widget):
|
class AlertRenderer:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
|
||||||
self.font_regular: rl.Font = gui_app.font(FontWeight.NORMAL)
|
self.font_regular: rl.Font = gui_app.font(FontWeight.NORMAL)
|
||||||
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||||
|
|
||||||
@@ -73,20 +73,18 @@ class AlertRenderer(Widget):
|
|||||||
# Check if selfdriveState messages have stopped arriving
|
# Check if selfdriveState messages have stopped arriving
|
||||||
if not sm.updated['selfdriveState']:
|
if not sm.updated['selfdriveState']:
|
||||||
recv_frame = sm.recv_frame['selfdriveState']
|
recv_frame = sm.recv_frame['selfdriveState']
|
||||||
time_since_onroad = (sm.frame - ui_state.started_frame) / DEFAULT_FPS
|
if (sm.frame - recv_frame) > 5 * DEFAULT_FPS:
|
||||||
|
# Check if waiting to start
|
||||||
|
if recv_frame < ui_state.started_frame:
|
||||||
|
return ALERT_STARTUP_PENDING
|
||||||
|
|
||||||
# 1. Never received selfdriveState since going onroad
|
# Handle selfdrive timeout
|
||||||
waiting_for_startup = recv_frame < ui_state.started_frame
|
if TICI:
|
||||||
if waiting_for_startup and time_since_onroad > 5:
|
ss_missing = time.monotonic() - sm.recv_time['selfdriveState']
|
||||||
return ALERT_STARTUP_PENDING
|
if ss_missing > SELFDRIVE_STATE_TIMEOUT:
|
||||||
|
if ss.enabled and (ss_missing - SELFDRIVE_STATE_TIMEOUT) < SELFDRIVE_UNRESPONSIVE_TIMEOUT:
|
||||||
# 2. Lost communication with selfdriveState after receiving it
|
return ALERT_CRITICAL_TIMEOUT
|
||||||
if TICI and not waiting_for_startup:
|
return ALERT_CRITICAL_REBOOT
|
||||||
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
|
# No alert if size is none
|
||||||
if ss.alertSize == 0:
|
if ss.alertSize == 0:
|
||||||
@@ -95,10 +93,10 @@ class AlertRenderer(Widget):
|
|||||||
# Return current alert
|
# Return current alert
|
||||||
return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize, status=ss.alertStatus)
|
return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize, status=ss.alertStatus)
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle) -> bool:
|
def draw(self, rect: rl.Rectangle, sm: messaging.SubMaster) -> None:
|
||||||
alert = self.get_alert(ui_state.sm)
|
alert = self.get_alert(sm)
|
||||||
if not alert:
|
if not alert:
|
||||||
return False
|
return
|
||||||
|
|
||||||
alert_rect = self._get_alert_rect(rect, alert.size)
|
alert_rect = self._get_alert_rect(rect, alert.size)
|
||||||
self._draw_background(alert_rect, alert)
|
self._draw_background(alert_rect, alert)
|
||||||
@@ -110,14 +108,13 @@ class AlertRenderer(Widget):
|
|||||||
alert_rect.height - 2 * ALERT_PADDING
|
alert_rect.height - 2 * ALERT_PADDING
|
||||||
)
|
)
|
||||||
self._draw_text(text_rect, alert)
|
self._draw_text(text_rect, alert)
|
||||||
return True
|
|
||||||
|
|
||||||
def _get_alert_rect(self, rect: rl.Rectangle, size: int) -> rl.Rectangle:
|
def _get_alert_rect(self, rect: rl.Rectangle, size: int) -> rl.Rectangle:
|
||||||
if size == log.SelfdriveState.AlertSize.full:
|
if size == log.SelfdriveState.AlertSize.full:
|
||||||
return rect
|
return rect
|
||||||
|
|
||||||
height = (ALERT_FONT_MEDIUM + 2 * ALERT_PADDING if size == log.SelfdriveState.AlertSize.small else
|
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)
|
ALERT_FONT_BIG + ALERT_LINE_SPACING + ALERT_FONT_SMALL + 2 * ALERT_PADDING)
|
||||||
|
|
||||||
return rl.Rectangle(
|
return rl.Rectangle(
|
||||||
rect.x + ALERT_MARGIN,
|
rect.x + ALERT_MARGIN,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
from collections.abc import Callable
|
|
||||||
from cereal import log
|
from cereal import log
|
||||||
from msgq.visionipc import VisionStreamType
|
from msgq.visionipc import VisionStreamType
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, UI_BORDER_SIZE
|
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, UI_BORDER_SIZE
|
||||||
@@ -13,6 +13,7 @@ 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.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame
|
||||||
from openpilot.common.transformations.orientation import rot_from_euler
|
from openpilot.common.transformations.orientation import rot_from_euler
|
||||||
|
|
||||||
|
|
||||||
OpState = log.SelfdriveState.OpenpilotState
|
OpState = log.SelfdriveState.OpenpilotState
|
||||||
CALIBRATED = log.LiveCalibrationData.Status.calibrated
|
CALIBRATED = log.LiveCalibrationData.Status.calibrated
|
||||||
ROAD_CAM = VisionStreamType.VISION_STREAM_ROAD
|
ROAD_CAM = VisionStreamType.VISION_STREAM_ROAD
|
||||||
@@ -25,9 +26,6 @@ BORDER_COLORS = {
|
|||||||
UIStatus.ENGAGED: rl.Color(0x17, 0x86, 0x44, 0xF1), # Green for engaged 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):
|
class AugmentedRoadView(CameraView):
|
||||||
def __init__(self, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_ROAD):
|
def __init__(self, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_ROAD):
|
||||||
@@ -49,19 +47,11 @@ class AugmentedRoadView(CameraView):
|
|||||||
self.alert_renderer = AlertRenderer()
|
self.alert_renderer = AlertRenderer()
|
||||||
self.driver_state_renderer = DriverStateRenderer()
|
self.driver_state_renderer = DriverStateRenderer()
|
||||||
|
|
||||||
# Callbacks
|
def render(self, rect):
|
||||||
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
|
# Only render when system is started to avoid invalid data access
|
||||||
if not ui_state.started:
|
if not ui_state.started:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._switch_stream_if_needed(ui_state.sm)
|
|
||||||
|
|
||||||
# Update calibration before rendering
|
# Update calibration before rendering
|
||||||
self._update_calibration()
|
self._update_calibration()
|
||||||
|
|
||||||
@@ -86,13 +76,13 @@ class AugmentedRoadView(CameraView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Render the base camera view
|
# Render the base camera view
|
||||||
super()._render(rect)
|
super().render(rect)
|
||||||
|
|
||||||
# Draw all UI overlays
|
# Draw all UI overlays
|
||||||
self.model_renderer.render(self._content_rect)
|
self.model_renderer.draw(self._content_rect, ui_state.sm)
|
||||||
self._hud_renderer.render(self._content_rect)
|
self._hud_renderer.draw(self._content_rect, ui_state.sm)
|
||||||
if not self.alert_renderer.render(self._content_rect):
|
self.alert_renderer.draw(self._content_rect, ui_state.sm)
|
||||||
self.driver_state_renderer.render(self._content_rect)
|
self.driver_state_renderer.draw(self._content_rect, ui_state.sm)
|
||||||
|
|
||||||
# Custom UI extension point - add custom overlays here
|
# Custom UI extension point - add custom overlays here
|
||||||
# Use self._content_rect for positioning within camera bounds
|
# Use self._content_rect for positioning within camera bounds
|
||||||
@@ -100,32 +90,10 @@ class AugmentedRoadView(CameraView):
|
|||||||
# End clipping region
|
# End clipping region
|
||||||
rl.end_scissor_mode()
|
rl.end_scissor_mode()
|
||||||
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
def _draw_border(self, rect: rl.Rectangle):
|
def _draw_border(self, rect: rl.Rectangle):
|
||||||
border_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED])
|
border_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED])
|
||||||
rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, border_color)
|
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):
|
def _update_calibration(self):
|
||||||
# Update device camera if not already set
|
# Update device camera if not already set
|
||||||
sm = ui_state.sm
|
sm = ui_state.sm
|
||||||
@@ -161,7 +129,7 @@ class AugmentedRoadView(CameraView):
|
|||||||
|
|
||||||
# Get camera configuration
|
# Get camera configuration
|
||||||
device_camera = self.device_camera or DEFAULT_DEVICE_CAMERA
|
device_camera = self.device_camera or DEFAULT_DEVICE_CAMERA
|
||||||
is_wide_camera = self.stream_type == WIDE_CAM
|
is_wide_camera = self.stream_type == VisionStreamType.VISION_STREAM_WIDE_ROAD
|
||||||
intrinsic = device_camera.ecam.intrinsics if is_wide_camera else device_camera.fcam.intrinsics
|
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
|
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
|
zoom = 2.0 if is_wide_camera else 1.1
|
||||||
@@ -202,9 +170,9 @@ class AugmentedRoadView(CameraView):
|
|||||||
])
|
])
|
||||||
|
|
||||||
video_transform = np.array([
|
video_transform = np.array([
|
||||||
[zoom, 0.0, (w / 2 + x - x_offset) - (cx * zoom)],
|
[zoom, 0.0, (w / 2 + x - x_offset) - (cx * zoom)],
|
||||||
[0.0, zoom, (h / 2 + y - y_offset) - (cy * zoom)],
|
[0.0, zoom, (h / 2 + y - y_offset) - (cy * zoom)],
|
||||||
[0.0, 0.0, 1.0]
|
[0.0, 0.0, 1.0]
|
||||||
])
|
])
|
||||||
self.model_renderer.set_transform(video_transform @ calib_transform)
|
self.model_renderer.set_transform(video_transform @ calib_transform)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import platform
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
|
||||||
@@ -7,21 +6,12 @@ from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
|
|||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
from openpilot.system.ui.lib.application import gui_app
|
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.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
|
CONNECTION_RETRY_INTERVAL = 0.2 # seconds between connection attempts
|
||||||
|
|
||||||
VERSION = """
|
VERTEX_SHADER = """
|
||||||
#version 300 es
|
#version 300 es
|
||||||
precision mediump float;
|
|
||||||
"""
|
|
||||||
if platform.system() == "Darwin":
|
|
||||||
VERSION = """
|
|
||||||
#version 330 core
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
VERTEX_SHADER = VERSION + """
|
|
||||||
in vec3 vertexPosition;
|
in vec3 vertexPosition;
|
||||||
in vec2 vertexTexCoord;
|
in vec2 vertexTexCoord;
|
||||||
in vec3 vertexNormal;
|
in vec3 vertexNormal;
|
||||||
@@ -51,7 +41,9 @@ if TICI:
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
FRAME_FRAGMENT_SHADER = VERSION + """
|
FRAME_FRAGMENT_SHADER = """
|
||||||
|
#version 300 es
|
||||||
|
precision mediump float;
|
||||||
in vec2 fragTexCoord;
|
in vec2 fragTexCoord;
|
||||||
uniform sampler2D texture0;
|
uniform sampler2D texture0;
|
||||||
uniform sampler2D texture1;
|
uniform sampler2D texture1;
|
||||||
@@ -63,10 +55,8 @@ else:
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class CameraView:
|
||||||
class CameraView(Widget):
|
|
||||||
def __init__(self, name: str, stream_type: VisionStreamType):
|
def __init__(self, name: str, stream_type: VisionStreamType):
|
||||||
super().__init__()
|
|
||||||
self._name = name
|
self._name = name
|
||||||
# Primary stream
|
# Primary stream
|
||||||
self.client = VisionIpcClient(name, stream_type, conflate=True)
|
self.client = VisionIpcClient(name, stream_type, conflate=True)
|
||||||
@@ -78,6 +68,7 @@ class CameraView(Widget):
|
|||||||
self._target_stream_type: VisionStreamType | None = None
|
self._target_stream_type: VisionStreamType | None = None
|
||||||
self._switching: bool = False
|
self._switching: bool = False
|
||||||
|
|
||||||
|
|
||||||
self._texture_needs_update = True
|
self._texture_needs_update = True
|
||||||
self.last_connection_attempt: float = 0.0
|
self.last_connection_attempt: float = 0.0
|
||||||
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAME_FRAGMENT_SHADER)
|
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAME_FRAGMENT_SHADER)
|
||||||
@@ -91,7 +82,7 @@ class CameraView(Widget):
|
|||||||
self.egl_images: dict[int, EGLImage] = {}
|
self.egl_images: dict[int, EGLImage] = {}
|
||||||
self.egl_texture: rl.Texture | None = None
|
self.egl_texture: rl.Texture | None = None
|
||||||
|
|
||||||
self._placeholder_color: rl.Color | None = None
|
self._placeholder_color : rl.Color | None = None
|
||||||
|
|
||||||
# Initialize EGL for zero-copy rendering on TICI
|
# Initialize EGL for zero-copy rendering on TICI
|
||||||
if TICI:
|
if TICI:
|
||||||
@@ -154,12 +145,12 @@ class CameraView(Widget):
|
|||||||
zy = min(widget_aspect_ratio / frame_aspect_ratio, 1.0)
|
zy = min(widget_aspect_ratio / frame_aspect_ratio, 1.0)
|
||||||
|
|
||||||
return np.array([
|
return np.array([
|
||||||
[zx, 0.0, 0.0],
|
[zx, 0.0, 0.0],
|
||||||
[0.0, zy, 0.0],
|
[0.0, zy, 0.0],
|
||||||
[0.0, 0.0, 1.0]
|
[0.0, 0.0, 1.0]
|
||||||
])
|
])
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle):
|
def render(self, rect: rl.Rectangle):
|
||||||
if self._switching:
|
if self._switching:
|
||||||
self._handle_switch()
|
self._handle_switch()
|
||||||
|
|
||||||
@@ -239,7 +230,7 @@ class CameraView(Widget):
|
|||||||
# Update textures with new frame data
|
# Update textures with new frame data
|
||||||
if self._texture_needs_update:
|
if self._texture_needs_update:
|
||||||
y_data = self.frame.data[: self.frame.uv_offset]
|
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_y, rl.ffi.cast("void *", y_data.ctypes.data))
|
||||||
rl.update_texture(self.texture_uv, rl.ffi.cast("void *", uv_data.ctypes.data))
|
rl.update_texture(self.texture_uv, rl.ffi.cast("void *", uv_data.ctypes.data))
|
||||||
@@ -274,7 +265,7 @@ class CameraView(Widget):
|
|||||||
def _handle_switch(self) -> None:
|
def _handle_switch(self) -> None:
|
||||||
"""Check if target stream is ready and switch immediately."""
|
"""Check if target stream is ready and switch immediately."""
|
||||||
if not self._target_client or not self._switching:
|
if not self._target_client or not self._switching:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Try to connect target if needed
|
# Try to connect target if needed
|
||||||
if not self._target_client.is_connected():
|
if not self._target_client.is_connected():
|
||||||
@@ -286,28 +277,28 @@ class CameraView(Widget):
|
|||||||
# Check if target has frames ready
|
# Check if target has frames ready
|
||||||
target_frame = self._target_client.recv(timeout_ms=0)
|
target_frame = self._target_client.recv(timeout_ms=0)
|
||||||
if target_frame:
|
if target_frame:
|
||||||
self.frame = target_frame # Update current frame to target frame
|
self.frame = target_frame # Update current frame to target frame
|
||||||
self._complete_switch()
|
self._complete_switch()
|
||||||
|
|
||||||
def _complete_switch(self) -> None:
|
def _complete_switch(self) -> None:
|
||||||
"""Instantly switch to target stream."""
|
"""Instantly switch to target stream."""
|
||||||
cloudlog.debug(f"Switching to {self._target_stream_type}")
|
cloudlog.debug(f"Switching to {self._target_stream_type}")
|
||||||
# Clean up current resources
|
# Clean up current resources
|
||||||
if self.client:
|
if self.client:
|
||||||
del self.client
|
del self.client
|
||||||
|
|
||||||
# Switch to target
|
# Switch to target
|
||||||
self.client = self._target_client
|
self.client = self._target_client
|
||||||
self._stream_type = self._target_stream_type
|
self._stream_type = self._target_stream_type
|
||||||
self._texture_needs_update = True
|
self._texture_needs_update = True
|
||||||
|
|
||||||
# Reset state
|
# Reset state
|
||||||
self._target_client = None
|
self._target_client = None
|
||||||
self._target_stream_type = None
|
self._target_stream_type = None
|
||||||
self._switching = False
|
self._switching = False
|
||||||
|
|
||||||
# Initialize textures for new stream
|
# Initialize textures for new stream
|
||||||
self._initialize_textures()
|
self._initialize_textures()
|
||||||
|
|
||||||
def _initialize_textures(self):
|
def _initialize_textures(self):
|
||||||
self._clear_textures()
|
self._clear_textures()
|
||||||
|
|||||||
+15
-19
@@ -1,23 +1,20 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
from cereal import messaging
|
||||||
from msgq.visionipc import VisionStreamType
|
from msgq.visionipc import VisionStreamType
|
||||||
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
|
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
|
||||||
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
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.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.label import gui_label
|
from openpilot.system.ui.lib.label import gui_label
|
||||||
|
|
||||||
|
|
||||||
class DriverCameraDialog(CameraView):
|
class DriverCameraView(CameraView):
|
||||||
def __init__(self):
|
def __init__(self, stream_type: VisionStreamType):
|
||||||
super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER)
|
super().__init__("camerad", stream_type)
|
||||||
self.driver_state_renderer = DriverStateRenderer()
|
self.driver_state_renderer = DriverStateRenderer()
|
||||||
|
|
||||||
def _render(self, rect):
|
def render(self, rect, sm):
|
||||||
super()._render(rect)
|
super().render(rect)
|
||||||
|
|
||||||
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if not self.frame:
|
if not self.frame:
|
||||||
gui_label(
|
gui_label(
|
||||||
@@ -27,15 +24,13 @@ class DriverCameraDialog(CameraView):
|
|||||||
font_weight=FontWeight.BOLD,
|
font_weight=FontWeight.BOLD,
|
||||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
)
|
)
|
||||||
return -1
|
return
|
||||||
|
|
||||||
self._draw_face_detection(rect)
|
self._draw_face_detection(rect, sm)
|
||||||
self.driver_state_renderer.render(rect)
|
self.driver_state_renderer.draw(rect, sm)
|
||||||
|
|
||||||
return -1
|
def _draw_face_detection(self, rect: rl.Rectangle, sm) -> None:
|
||||||
|
driver_state = sm["driverStateV2"]
|
||||||
def _draw_face_detection(self, rect: rl.Rectangle) -> None:
|
|
||||||
driver_state = ui_state.sm["driverStateV2"]
|
|
||||||
is_rhd = driver_state.wheelOnRightProb > 0.5
|
is_rhd = driver_state.wheelOnRightProb > 0.5
|
||||||
driver_data = driver_state.rightDriverData if is_rhd else driver_state.leftDriverData
|
driver_data = driver_state.rightDriverData if is_rhd else driver_state.leftDriverData
|
||||||
face_detect = driver_data.faceProb > 0.7
|
face_detect = driver_data.faceProb > 0.7
|
||||||
@@ -88,11 +83,12 @@ class DriverCameraDialog(CameraView):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
gui_app.init_window("Driver Camera View")
|
gui_app.init_window("Driver Camera View")
|
||||||
|
sm = messaging.SubMaster(["selfdriveState", "driverStateV2", "driverMonitoringState"])
|
||||||
|
|
||||||
driver_camera_view = DriverCameraDialog()
|
driver_camera_view = DriverCameraView(VisionStreamType.VISION_STREAM_DRIVER)
|
||||||
try:
|
try:
|
||||||
for _ in gui_app.render():
|
for _ in gui_app.render():
|
||||||
ui_state.update()
|
sm.update()
|
||||||
driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height), sm)
|
||||||
finally:
|
finally:
|
||||||
driver_camera_view.close()
|
driver_camera_view.close()
|
||||||
@@ -3,19 +3,19 @@ import pyray as rl
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, UI_BORDER_SIZE
|
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.application import gui_app
|
||||||
from openpilot.system.ui.lib.widget import Widget
|
|
||||||
|
|
||||||
# Default 3D coordinates for face keypoints as a NumPy array
|
# Default 3D coordinates for face keypoints as a NumPy array
|
||||||
DEFAULT_FACE_KPTS_3D = np.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],
|
[-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],
|
[-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],
|
[-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],
|
[-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],
|
[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],
|
[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],
|
[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],
|
[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],
|
||||||
], dtype=np.float32)
|
], dtype=np.float32)
|
||||||
|
|
||||||
# UI constants
|
# UI constants
|
||||||
@@ -31,7 +31,6 @@ SCALES_NEG = np.array([0.7, 0.4, 0.4], dtype=np.float32)
|
|||||||
ARC_POINT_COUNT = 37 # Number of points in the arc
|
ARC_POINT_COUNT = 37 # Number of points in the arc
|
||||||
ARC_ANGLES = np.linspace(0.0, np.pi, ARC_POINT_COUNT, dtype=np.float32)
|
ARC_ANGLES = np.linspace(0.0, np.pi, ARC_POINT_COUNT, dtype=np.float32)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ArcData:
|
class ArcData:
|
||||||
"""Data structure for arc rendering parameters."""
|
"""Data structure for arc rendering parameters."""
|
||||||
@@ -41,15 +40,14 @@ class ArcData:
|
|||||||
height: float
|
height: float
|
||||||
thickness: float
|
thickness: float
|
||||||
|
|
||||||
|
class DriverStateRenderer:
|
||||||
class DriverStateRenderer(Widget):
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
|
||||||
# Initial state with NumPy arrays
|
# Initial state with NumPy arrays
|
||||||
self.face_kpts_draw = DEFAULT_FACE_KPTS_3D.copy()
|
self.face_kpts_draw = DEFAULT_FACE_KPTS_3D.copy()
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
self.is_rhd = False
|
self.is_rhd = False
|
||||||
self.dm_fade_state = 0.0
|
self.dm_fade_state = 0.0
|
||||||
|
self.state_updated = False
|
||||||
self.last_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
|
self.last_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
|
||||||
self.driver_pose_vals = np.zeros(3, dtype=np.float32)
|
self.driver_pose_vals = np.zeros(3, dtype=np.float32)
|
||||||
self.driver_pose_diff = np.zeros(3, dtype=np.float32)
|
self.driver_pose_diff = np.zeros(3, dtype=np.float32)
|
||||||
@@ -75,10 +73,14 @@ class DriverStateRenderer(Widget):
|
|||||||
self.engaged_color = rl.Color(26, 242, 66, 255)
|
self.engaged_color = rl.Color(26, 242, 66, 255)
|
||||||
self.disengaged_color = rl.Color(139, 139, 139, 255)
|
self.disengaged_color = rl.Color(139, 139, 139, 255)
|
||||||
|
|
||||||
self.set_visible(lambda: (ui_state.sm.recv_frame['driverStateV2'] > ui_state.started_frame and
|
def draw(self, rect, sm):
|
||||||
ui_state.sm.seen['driverMonitoringState']))
|
if not self._is_visible(sm):
|
||||||
|
return
|
||||||
|
|
||||||
|
self._update_state(sm, rect)
|
||||||
|
if not self.state_updated:
|
||||||
|
return
|
||||||
|
|
||||||
def _render(self, rect):
|
|
||||||
# Set opacity based on active state
|
# Set opacity based on active state
|
||||||
opacity = 0.65 if self.is_active else 0.2
|
opacity = 0.65 if self.is_active else 0.2
|
||||||
|
|
||||||
@@ -103,14 +105,18 @@ class DriverStateRenderer(Widget):
|
|||||||
if self.v_arc_data:
|
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)
|
rl.draw_spline_linear(self.v_arc_lines, len(self.v_arc_lines), self.v_arc_data.thickness, self.arc_color)
|
||||||
|
|
||||||
def _update_state(self):
|
def _is_visible(self, sm):
|
||||||
|
"""Check if the visualization should be rendered."""
|
||||||
|
return (sm.recv_frame['driverStateV2'] > ui_state.started_frame and
|
||||||
|
sm.seen['driverMonitoringState'] and
|
||||||
|
sm['selfdriveState'].alertSize == 0)
|
||||||
|
|
||||||
|
def _update_state(self, sm, rect):
|
||||||
"""Update the driver monitoring state based on model data"""
|
"""Update the driver monitoring state based on model data"""
|
||||||
sm = ui_state.sm
|
if not sm.updated["driverMonitoringState"]:
|
||||||
if not sm.updated["driverMonitoringState"]:
|
if self.state_updated and (rect.x != self.last_rect.x or rect.y != self.last_rect.y or \
|
||||||
if (self._rect.x != self.last_rect.x or self._rect.y != self.last_rect.y or
|
rect.width != self.last_rect.width or rect.height != self.last_rect.height):
|
||||||
self._rect.width != self.last_rect.width or self._rect.height != self.last_rect.height):
|
self._pre_calculate_drawing_elements(rect)
|
||||||
self._pre_calculate_drawing_elements()
|
|
||||||
self.last_rect = self._rect
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get monitoring state
|
# Get monitoring state
|
||||||
@@ -159,15 +165,16 @@ class DriverStateRenderer(Widget):
|
|||||||
self.face_keypoints_transformed = self.face_kpts_draw[:, :2] * kp_depth[:, None]
|
self.face_keypoints_transformed = self.face_kpts_draw[:, :2] * kp_depth[:, None]
|
||||||
|
|
||||||
# Pre-calculate all drawing elements
|
# Pre-calculate all drawing elements
|
||||||
self._pre_calculate_drawing_elements()
|
self._pre_calculate_drawing_elements(rect)
|
||||||
|
self.state_updated = True
|
||||||
|
|
||||||
def _pre_calculate_drawing_elements(self):
|
def _pre_calculate_drawing_elements(self, rect):
|
||||||
"""Pre-calculate all drawing elements based on the current rectangle"""
|
"""Pre-calculate all drawing elements based on the current rectangle"""
|
||||||
# Calculate icon position (bottom-left or bottom-right)
|
# Calculate icon position (bottom-left or bottom-right)
|
||||||
width, height = self._rect.width, self._rect.height
|
width, height = rect.width, rect.height
|
||||||
offset = UI_BORDER_SIZE + BTN_SIZE // 2
|
offset = UI_BORDER_SIZE + BTN_SIZE // 2
|
||||||
self.position_x = self._rect.x + (width - offset if self.is_rhd else offset)
|
self.position_x = rect.x + (width - offset if self.is_rhd else offset)
|
||||||
self.position_y = self._rect.y + height - offset
|
self.position_y = rect.y + height - offset
|
||||||
|
|
||||||
# Pre-calculate the face lines positions
|
# Pre-calculate the face lines positions
|
||||||
positioned_keypoints = self.face_keypoints_transformed + np.array([self.position_x, self.position_y])
|
positioned_keypoints = self.face_keypoints_transformed + np.array([self.position_x, self.position_y])
|
||||||
@@ -182,15 +189,15 @@ class DriverStateRenderer(Widget):
|
|||||||
# Horizontal arc
|
# Horizontal arc
|
||||||
h_width = abs(delta_x)
|
h_width = abs(delta_x)
|
||||||
self.h_arc_data = self._calculate_arc_data(
|
self.h_arc_data = self._calculate_arc_data(
|
||||||
delta_x, h_width, self.position_x, self.position_y - ARC_LENGTH / 2,
|
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
|
self.driver_pose_sins[1], self.driver_pose_diff[1], is_horizontal=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Vertical arc
|
# Vertical arc
|
||||||
v_height = abs(delta_y)
|
v_height = abs(delta_y)
|
||||||
self.v_arc_data = self._calculate_arc_data(
|
self.v_arc_data = self._calculate_arc_data(
|
||||||
delta_y, v_height, self.position_x - ARC_LENGTH / 2, self.position_y,
|
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
|
self.driver_pose_sins[0], self.driver_pose_diff[0], is_horizontal=False
|
||||||
)
|
)
|
||||||
|
|
||||||
def _calculate_arc_data(
|
def _calculate_arc_data(
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
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,11 +1,10 @@
|
|||||||
import pyray as rl
|
import pyray as rl
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from cereal.messaging import SubMaster
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
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.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.common.conversions import Conversions as CV
|
from openpilot.common.conversions import Conversions as CV
|
||||||
from openpilot.system.ui.lib.widget import Widget
|
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
SET_SPEED_NA = 255
|
SET_SPEED_NA = 255
|
||||||
@@ -55,25 +54,21 @@ FONT_SIZES = FontSizes()
|
|||||||
COLORS = Colors()
|
COLORS = Colors()
|
||||||
|
|
||||||
|
|
||||||
class HudRenderer(Widget):
|
class HudRenderer:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
|
||||||
"""Initialize the HUD renderer."""
|
"""Initialize the HUD renderer."""
|
||||||
self.is_cruise_set: bool = False
|
self.is_cruise_set: bool = False
|
||||||
self.is_cruise_available: bool = False
|
self.is_cruise_available: bool = False
|
||||||
self.set_speed: float = SET_SPEED_NA
|
self.set_speed: float = SET_SPEED_NA
|
||||||
self.speed: float = 0.0
|
self.speed: float = 0.0
|
||||||
self.v_ego_cluster_seen: bool = False
|
self.v_ego_cluster_seen: bool = False
|
||||||
|
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_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
|
||||||
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||||
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
|
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
|
||||||
|
|
||||||
self._exp_button = ExpButton(UI_CONFIG.button_size, UI_CONFIG.wheel_icon_size)
|
def _update_state(self, sm: SubMaster) -> None:
|
||||||
|
|
||||||
def _update_state(self) -> None:
|
|
||||||
"""Update HUD state based on car state and controls state."""
|
"""Update HUD state based on car state and controls state."""
|
||||||
sm = ui_state.sm
|
|
||||||
if sm.recv_frame["carState"] < ui_state.started_frame:
|
if sm.recv_frame["carState"] < ui_state.started_frame:
|
||||||
self.is_cruise_set = False
|
self.is_cruise_set = False
|
||||||
self.set_speed = SET_SPEED_NA
|
self.set_speed = SET_SPEED_NA
|
||||||
@@ -99,9 +94,9 @@ class HudRenderer(Widget):
|
|||||||
speed_conversion = CV.MS_TO_KPH if ui_state.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)
|
self.speed = max(0.0, v_ego * speed_conversion)
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle) -> None:
|
def draw(self, rect: rl.Rectangle, sm: SubMaster) -> None:
|
||||||
"""Render HUD elements to the screen."""
|
"""Render HUD elements to the screen."""
|
||||||
# Draw the header background
|
self._update_state(sm)
|
||||||
rl.draw_rectangle_gradient_v(
|
rl.draw_rectangle_gradient_v(
|
||||||
int(rect.x),
|
int(rect.x),
|
||||||
int(rect.y),
|
int(rect.y),
|
||||||
@@ -115,13 +110,7 @@ class HudRenderer(Widget):
|
|||||||
self._draw_set_speed(rect)
|
self._draw_set_speed(rect)
|
||||||
|
|
||||||
self._draw_current_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:
|
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
|
||||||
"""Draw the MAX speed indicator box."""
|
"""Draw the MAX speed indicator box."""
|
||||||
@@ -177,3 +166,13 @@ class HudRenderer(Widget):
|
|||||||
unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit)
|
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)
|
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)
|
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 ui_state.status == UIStatus.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)))
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ from openpilot.common.params import Params
|
|||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.system.ui.lib.application import DEFAULT_FPS
|
from openpilot.system.ui.lib.application import DEFAULT_FPS
|
||||||
from openpilot.system.ui.lib.shader_polygon import draw_polygon
|
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
|
from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT
|
||||||
|
|
||||||
|
|
||||||
CLIP_MARGIN = 500
|
CLIP_MARGIN = 500
|
||||||
MIN_DRAW_DISTANCE = 10.0
|
MIN_DRAW_DISTANCE = 10.0
|
||||||
MAX_DRAW_DISTANCE = 100.0
|
MAX_DRAW_DISTANCE = 100.0
|
||||||
@@ -36,7 +36,6 @@ class ModelPoints:
|
|||||||
raw_points: np.ndarray = field(default_factory=lambda: np.empty((0, 3), dtype=np.float32))
|
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))
|
projected_points: np.ndarray = field(default_factory=lambda: np.empty((0, 2), dtype=np.float32))
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LeadVehicle:
|
class LeadVehicle:
|
||||||
glow: list[float] = field(default_factory=list)
|
glow: list[float] = field(default_factory=list)
|
||||||
@@ -44,9 +43,8 @@ class LeadVehicle:
|
|||||||
fill_alpha: int = 0
|
fill_alpha: int = 0
|
||||||
|
|
||||||
|
|
||||||
class ModelRenderer(Widget):
|
class ModelRenderer:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
|
||||||
self._longitudinal_control = False
|
self._longitudinal_control = False
|
||||||
self._experimental_mode = False
|
self._experimental_mode = False
|
||||||
self._blend_factor = 1.0
|
self._blend_factor = 1.0
|
||||||
@@ -66,6 +64,11 @@ class ModelRenderer(Widget):
|
|||||||
self._car_space_transform = np.zeros((3, 3), dtype=np.float32)
|
self._car_space_transform = np.zeros((3, 3), dtype=np.float32)
|
||||||
self._transform_dirty = True
|
self._transform_dirty = True
|
||||||
self._clip_region = None
|
self._clip_region = None
|
||||||
|
self._rect = None
|
||||||
|
|
||||||
|
# Pre-allocated arrays for polygon conversion
|
||||||
|
self._temp_points_3d = np.empty((MAX_POINTS * 2, 3), dtype=np.float32)
|
||||||
|
self._temp_proj = np.empty((3, MAX_POINTS * 2), dtype=np.float32)
|
||||||
|
|
||||||
self._exp_gradient = {
|
self._exp_gradient = {
|
||||||
'start': (0.0, 1.0), # Bottom of path
|
'start': (0.0, 1.0), # Bottom of path
|
||||||
@@ -83,15 +86,14 @@ class ModelRenderer(Widget):
|
|||||||
self._car_space_transform = transform.astype(np.float32)
|
self._car_space_transform = transform.astype(np.float32)
|
||||||
self._transform_dirty = True
|
self._transform_dirty = True
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle):
|
def draw(self, rect: rl.Rectangle, sm: messaging.SubMaster):
|
||||||
sm = ui_state.sm
|
|
||||||
|
|
||||||
# Check if data is up-to-date
|
# Check if data is up-to-date
|
||||||
if (sm.recv_frame["liveCalibration"] < ui_state.started_frame or
|
if (sm.recv_frame["liveCalibration"] < ui_state.started_frame or
|
||||||
sm.recv_frame["modelV2"] < ui_state.started_frame):
|
sm.recv_frame["modelV2"] < ui_state.started_frame):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Set up clipping region
|
# Set up clipping region
|
||||||
|
self._rect = rect
|
||||||
self._clip_region = rl.Rectangle(
|
self._clip_region = rl.Rectangle(
|
||||||
rect.x - CLIP_MARGIN, rect.y - CLIP_MARGIN, rect.width + 2 * CLIP_MARGIN, rect.height + 2 * CLIP_MARGIN
|
rect.x - CLIP_MARGIN, rect.y - CLIP_MARGIN, rect.width + 2 * CLIP_MARGIN, rect.height + 2 * CLIP_MARGIN
|
||||||
)
|
)
|
||||||
@@ -125,6 +127,7 @@ class ModelRenderer(Widget):
|
|||||||
self._update_leads(radar_state, path_x_array)
|
self._update_leads(radar_state, path_x_array)
|
||||||
self._transform_dirty = False
|
self._transform_dirty = False
|
||||||
|
|
||||||
|
|
||||||
# Draw elements
|
# Draw elements
|
||||||
self._draw_lane_lines()
|
self._draw_lane_lines()
|
||||||
self._draw_path(sm)
|
self._draw_path(sm)
|
||||||
@@ -253,7 +256,7 @@ class ModelRenderer(Widget):
|
|||||||
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)]
|
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)]
|
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):
|
def _draw_lane_lines(self):
|
||||||
"""Draw lane lines and road edges"""
|
"""Draw lane lines and road edges"""
|
||||||
@@ -354,60 +357,54 @@ class ModelRenderer(Widget):
|
|||||||
if points.shape[0] == 0:
|
if points.shape[0] == 0:
|
||||||
return np.empty((0, 2), dtype=np.float32)
|
return np.empty((0, 2), dtype=np.float32)
|
||||||
|
|
||||||
N = points.shape[0]
|
# Create left and right 3D points in one array
|
||||||
# Generate left and right 3D points in one array using broadcasting
|
n_points = points.shape[0]
|
||||||
offsets = np.array([[0, -y_off, z_off], [0, y_off, z_off]], dtype=np.float32)
|
points_3d = self._temp_points_3d[:n_points * 2]
|
||||||
points_3d = points[None, :, :] + offsets[:, None, :] # Shape: 2xNx3
|
points_3d[:n_points, 0] = points_3d[n_points:, 0] = points[:, 0]
|
||||||
points_3d = points_3d.reshape(2 * N, 3) # Shape: (2*N)x3
|
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
|
||||||
|
|
||||||
# Transform all points to projected space in one operation
|
# Single matrix multiplication for projections
|
||||||
proj = self._car_space_transform @ points_3d.T # Shape: 3x(2*N)
|
proj = np.ascontiguousarray(self._temp_proj[:, :n_points * 2]) # Slice the pre-allocated array
|
||||||
proj = proj.reshape(3, 2, N)
|
np.dot(self._car_space_transform, points_3d.T, out=proj)
|
||||||
left_proj = proj[:, 0, :]
|
valid_z = np.abs(proj[2]) > 1e-6
|
||||||
right_proj = proj[:, 1, :]
|
if not np.any(valid_z):
|
||||||
|
|
||||||
# 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)
|
return np.empty((0, 2), dtype=np.float32)
|
||||||
|
|
||||||
# Compute screen coordinates
|
# Compute screen coordinates
|
||||||
left_screen = left_proj[:2, valid_proj] / left_proj[2, valid_proj][None, :]
|
screen = proj[:2, valid_z] / proj[2, valid_z][None, :]
|
||||||
right_screen = right_proj[:2, valid_proj] / right_proj[2, valid_proj][None, :]
|
left_screen = screen[:, :n_points].T
|
||||||
|
right_screen = screen[:, n_points:].T
|
||||||
|
|
||||||
# Define clip region bounds
|
# Ensure consistent shapes by re-aligning valid points
|
||||||
clip = self._clip_region
|
valid_points = np.minimum(left_screen.shape[0], right_screen.shape[0])
|
||||||
x_min, x_max = clip.x, clip.x + clip.width
|
if valid_points == 0:
|
||||||
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)
|
return np.empty((0, 2), dtype=np.float32)
|
||||||
|
left_screen = left_screen[:valid_points]
|
||||||
|
right_screen = right_screen[:valid_points]
|
||||||
|
|
||||||
# Select valid and clipped points
|
if self._clip_region:
|
||||||
left_screen = left_screen[:, both_in_clip]
|
clip = self._clip_region
|
||||||
right_screen = right_screen[:, both_in_clip]
|
bounds_mask = (
|
||||||
|
(left_screen[:, 0] >= clip.x) & (left_screen[:, 0] <= clip.x + clip.width) &
|
||||||
# Handle Y-coordinate inversion on hills
|
(left_screen[:, 1] >= clip.y) & (left_screen[:, 1] <= clip.y + clip.height) &
|
||||||
if not allow_invert and left_screen.shape[1] > 1:
|
(right_screen[:, 0] >= clip.x) & (right_screen[:, 0] <= clip.x + clip.width) &
|
||||||
y = left_screen[1, :] # y-coordinates
|
(right_screen[:, 1] >= clip.y) & (right_screen[:, 1] <= clip.y + clip.height)
|
||||||
keep = y == np.minimum.accumulate(y)
|
)
|
||||||
if not np.any(keep):
|
if not np.any(bounds_mask):
|
||||||
return np.empty((0, 2), dtype=np.float32)
|
return np.empty((0, 2), dtype=np.float32)
|
||||||
left_screen = left_screen[:, keep]
|
left_screen = left_screen[bounds_mask]
|
||||||
right_screen = right_screen[:, keep]
|
right_screen = right_screen[bounds_mask]
|
||||||
|
|
||||||
return np.vstack((left_screen.T, right_screen[:, ::-1].T)).astype(np.float32)
|
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)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _map_val(x, x0, x1, y0, y1):
|
def _map_val(x, x0, x1, y0, y1):
|
||||||
@@ -420,10 +417,10 @@ class ModelRenderer(Widget):
|
|||||||
def _hsla_to_color(h, s, l, a):
|
def _hsla_to_color(h, s, l, a):
|
||||||
rgb = colorsys.hls_to_rgb(h, l, s)
|
rgb = colorsys.hls_to_rgb(h, l, s)
|
||||||
return rl.Color(
|
return rl.Color(
|
||||||
int(rgb[0] * 255),
|
int(rgb[0] * 255),
|
||||||
int(rgb[1] * 255),
|
int(rgb[1] * 255),
|
||||||
int(rgb[2] * 255),
|
int(rgb[2] * 255),
|
||||||
int(a * 255)
|
int(a * 255)
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ FirehosePanel::FirehosePanel(SettingsWindow *parent) : QWidget((QWidget*)parent)
|
|||||||
layout->setSpacing(20);
|
layout->setSpacing(20);
|
||||||
|
|
||||||
// header
|
// 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';");
|
title->setStyleSheet("font-size: 100px; font-weight: 500; font-family: 'Noto Color Emoji';");
|
||||||
layout->addWidget(title, 0, Qt::AlignCenter);
|
layout->addWidget(title, 0, Qt::AlignCenter);
|
||||||
|
|
||||||
|
|||||||
@@ -139,15 +139,6 @@ void TogglesPanel::expandToggleDescription(const QString ¶m) {
|
|||||||
toggles[param.toStdString()]->showDescription();
|
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) {
|
void TogglesPanel::showEvent(QShowEvent *event) {
|
||||||
updateToggles();
|
updateToggles();
|
||||||
}
|
}
|
||||||
@@ -231,7 +222,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
|
|||||||
addItem(dcamBtn);
|
addItem(dcamBtn);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), "");
|
auto resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), "");
|
||||||
connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription);
|
connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription);
|
||||||
connect(resetCalibBtn, &ButtonControl::clicked, [&]() {
|
connect(resetCalibBtn, &ButtonControl::clicked, [&]() {
|
||||||
if (!uiState()->engaged()) {
|
if (!uiState()->engaged()) {
|
||||||
@@ -244,7 +235,6 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
|
|||||||
params.remove("LiveParametersV2");
|
params.remove("LiveParametersV2");
|
||||||
params.remove("LiveDelay");
|
params.remove("LiveDelay");
|
||||||
params.putBool("OnroadCycleRequested", true);
|
params.putBool("OnroadCycleRequested", true);
|
||||||
updateCalibDescription();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -323,7 +313,9 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DevicePanel::updateCalibDescription() {
|
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.");
|
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.");
|
||||||
std::string calib_bytes = params.get("CalibrationParams");
|
std::string calib_bytes = params.get("CalibrationParams");
|
||||||
if (!calib_bytes.empty()) {
|
if (!calib_bytes.empty()) {
|
||||||
try {
|
try {
|
||||||
@@ -341,53 +333,8 @@ void DevicePanel::updateCalibDescription() {
|
|||||||
qInfo() << "invalid CalibrationParams";
|
qInfo() << "invalid CalibrationParams";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
desc += tr(" Resetting calibration will restart openpilot if the car is powered on.");
|
||||||
const bool is_release = params.getBool("IsReleaseBranch");
|
qobject_cast<ButtonControl *>(sender())->setDescription(desc);
|
||||||
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() {
|
void DevicePanel::reboot() {
|
||||||
@@ -440,7 +387,6 @@ void SettingsWindow::setCurrentPanel(int index, const QString ¶m) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
emit expandToggleDescription(param);
|
emit expandToggleDescription(param);
|
||||||
emit scrollToToggle(param);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,7 +427,6 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) {
|
|||||||
|
|
||||||
TogglesPanel *toggles = new TogglesPanel(this);
|
TogglesPanel *toggles = new TogglesPanel(this);
|
||||||
QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription);
|
QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription);
|
||||||
QObject::connect(this, &SettingsWindow::scrollToToggle, toggles, &TogglesPanel::scrollToToggle);
|
|
||||||
|
|
||||||
auto networking = new Networking(this);
|
auto networking = new Networking(this);
|
||||||
QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType);
|
QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType);
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ signals:
|
|||||||
void reviewTrainingGuide();
|
void reviewTrainingGuide();
|
||||||
void showDriverView();
|
void showDriverView();
|
||||||
void expandToggleDescription(const QString ¶m);
|
void expandToggleDescription(const QString ¶m);
|
||||||
void scrollToToggle(const QString ¶m);
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QPushButton *sidebar_alert_widget;
|
QPushButton *sidebar_alert_widget;
|
||||||
@@ -68,7 +67,6 @@ protected slots:
|
|||||||
protected:
|
protected:
|
||||||
Params params;
|
Params params;
|
||||||
ButtonControl *pair_device;
|
ButtonControl *pair_device;
|
||||||
ButtonControl *resetCalibBtn;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class TogglesPanel : public ListWidget {
|
class TogglesPanel : public ListWidget {
|
||||||
@@ -79,7 +77,6 @@ public:
|
|||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void expandToggleDescription(const QString ¶m);
|
void expandToggleDescription(const QString ¶m);
|
||||||
void scrollToToggle(const QString ¶m);
|
|
||||||
|
|
||||||
protected slots:
|
protected slots:
|
||||||
virtual void updateState(const UIState &s);
|
virtual void updateState(const UIState &s);
|
||||||
|
|||||||
@@ -136,59 +136,6 @@ QWidget * Setup::low_voltage() {
|
|||||||
return widget;
|
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 * Setup::getting_started() {
|
||||||
QWidget *widget = new QWidget();
|
QWidget *widget = new QWidget();
|
||||||
|
|
||||||
@@ -358,17 +305,20 @@ QWidget * Setup::software_selection() {
|
|||||||
blayout->addWidget(cont);
|
blayout->addWidget(cont);
|
||||||
|
|
||||||
QObject::connect(cont, &QPushButton::clicked, [=]() {
|
QObject::connect(cont, &QPushButton::clicked, [=]() {
|
||||||
|
auto w = currentWidget();
|
||||||
|
QTimer::singleShot(0, [=]() {
|
||||||
|
setCurrentWidget(downloading_widget);
|
||||||
|
});
|
||||||
|
QString url = OPENPILOT_URL;
|
||||||
if (group->checkedButton() != openpilot) {
|
if (group->checkedButton() != openpilot) {
|
||||||
QTimer::singleShot(0, [=]() {
|
url = InputDialog::getText(tr("Enter URL"), this, tr("for Custom Software"));
|
||||||
setCurrentWidget(custom_software_warning_widget);
|
}
|
||||||
|
if (!url.isEmpty()) {
|
||||||
|
QTimer::singleShot(1000, this, [=]() {
|
||||||
|
download(url);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
QTimer::singleShot(0, [=]() {
|
setCurrentWidget(w);
|
||||||
setCurrentWidget(downloading_widget);
|
|
||||||
});
|
|
||||||
QTimer::singleShot(1000, this, [=]() {
|
|
||||||
download(OPENPILOT_URL);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -465,10 +415,8 @@ Setup::Setup(QWidget *parent) : QStackedWidget(parent) {
|
|||||||
|
|
||||||
addWidget(getting_started());
|
addWidget(getting_started());
|
||||||
addWidget(network_setup());
|
addWidget(network_setup());
|
||||||
software_selection_widget = software_selection();
|
addWidget(software_selection());
|
||||||
addWidget(software_selection_widget);
|
|
||||||
custom_software_warning_widget = custom_software_warning();
|
|
||||||
addWidget(custom_software_warning_widget);
|
|
||||||
downloading_widget = downloading();
|
downloading_widget = downloading();
|
||||||
addWidget(downloading_widget);
|
addWidget(downloading_widget);
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ public:
|
|||||||
private:
|
private:
|
||||||
void selectLanguage();
|
void selectLanguage();
|
||||||
QWidget *low_voltage();
|
QWidget *low_voltage();
|
||||||
QWidget *custom_software_warning();
|
|
||||||
QWidget *getting_started();
|
QWidget *getting_started();
|
||||||
QWidget *network_setup();
|
QWidget *network_setup();
|
||||||
QWidget *software_selection();
|
QWidget *software_selection();
|
||||||
@@ -24,8 +23,6 @@ private:
|
|||||||
|
|
||||||
QWidget *failed_widget;
|
QWidget *failed_widget;
|
||||||
QWidget *downloading_widget;
|
QWidget *downloading_widget;
|
||||||
QWidget *custom_software_warning_widget;
|
|
||||||
QWidget *software_selection_widget;
|
|
||||||
QTranslator translator;
|
QTranslator translator;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
|||||||
@@ -7,55 +7,21 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QStyle>
|
|
||||||
|
|
||||||
#include "common/model.h"
|
#include "common/model.h"
|
||||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h"
|
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h"
|
||||||
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.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) {
|
ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) {
|
||||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||||
main_layout->setContentsMargins(50, 20, 50, 20);
|
main_layout->setContentsMargins(50, 20, 50, 20);
|
||||||
|
|
||||||
ListWidgetSP *list = new ListWidgetSP(this, false);
|
ListWidgetSP *list = new ListWidgetSP(this);
|
||||||
ScrollViewSP *scroller = new ScrollViewSP(list, this);
|
ScrollViewSP *scroller = new ScrollViewSP(list, this);
|
||||||
main_layout->addWidget(scroller);
|
main_layout->addWidget(scroller);
|
||||||
|
|
||||||
const auto current_model = GetActiveModelName();
|
const auto current_model = GetActiveModelName();
|
||||||
currentModelLblBtn = new ButtonControlSP(tr("Current Model"), tr("SELECT"), "", this);
|
currentModelLblBtn = new ButtonControlSP(tr("Current Model"), tr("SELECT"), current_model);
|
||||||
currentModelLblBtn->setValue(current_model);
|
currentModelLblBtn->setValue(current_model);
|
||||||
|
|
||||||
connect(currentModelLblBtn, &ButtonControlSP::clicked, this, &ModelsPanel::handleCurrentModelLblBtnClicked);
|
connect(currentModelLblBtn, &ButtonControlSP::clicked, this, &ModelsPanel::handleCurrentModelLblBtnClicked);
|
||||||
@@ -66,29 +32,6 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) {
|
|||||||
connect(uiStateSP(), &UIStateSP::uiUpdate, this, &ModelsPanel::updateLabels);
|
connect(uiStateSP(), &UIStateSP::uiUpdate, this, &ModelsPanel::updateLabels);
|
||||||
list->addItem(currentModelLblBtn);
|
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
|
// LiveDelay toggle
|
||||||
list->addItem(new ParamControlSP("LagdToggle",
|
list->addItem(new ParamControlSP("LagdToggle",
|
||||||
tr("Live Learning Steer Delay"),
|
tr("Live Learning Steer Delay"),
|
||||||
@@ -97,38 +40,15 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) {
|
|||||||
"../assets/offroad/icon_shell.png"));
|
"../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
|
* @brief Updates the UI with bundle download progress information
|
||||||
* Reads status from modelManagerSP cereal message and displays status for all models
|
* Reads status from modelManagerSP cereal message and displays status for all models
|
||||||
*/
|
*/
|
||||||
void ModelsPanel::handleBundleDownloadProgress() {
|
void ModelsPanel::handleBundleDownloadProgress() {
|
||||||
supercomboFrame->setVisible(false);
|
|
||||||
visionFrame->setVisible(false);
|
|
||||||
policyFrame->setVisible(false);
|
|
||||||
navigationFrame->setVisible(false);
|
|
||||||
|
|
||||||
using DS = cereal::ModelManagerSP::DownloadStatus;
|
using DS = cereal::ModelManagerSP::DownloadStatus;
|
||||||
if (!model_manager.hasSelectedBundle() && !model_manager.hasActiveBundle()) {
|
if (!model_manager.hasSelectedBundle() && !model_manager.hasActiveBundle()) {
|
||||||
|
currentModelLblBtn->setDescription(tr("No custom model selected!"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,27 +61,21 @@ void ModelsPanel::handleBundleDownloadProgress() {
|
|||||||
|
|
||||||
// Get status for each model type in order
|
// Get status for each model type in order
|
||||||
for (const auto &model: models) {
|
for (const auto &model: models) {
|
||||||
|
QString typeName;
|
||||||
QString modelName = QString::fromStdString(bundle.getDisplayName());
|
QString modelName = QString::fromStdString(bundle.getDisplayName());
|
||||||
|
|
||||||
QProgressBar *progressBar = nullptr;
|
|
||||||
QFrame *modelFrame = nullptr;
|
|
||||||
|
|
||||||
switch (model.getType()) {
|
switch (model.getType()) {
|
||||||
case cereal::ModelManagerSP::Model::Type::SUPERCOMBO:
|
case cereal::ModelManagerSP::Model::Type::SUPERCOMBO:
|
||||||
progressBar = supercomboProgressBar;
|
typeName = tr("Driving");
|
||||||
modelFrame = supercomboFrame;
|
|
||||||
break;
|
break;
|
||||||
case cereal::ModelManagerSP::Model::Type::NAVIGATION:
|
case cereal::ModelManagerSP::Model::Type::NAVIGATION:
|
||||||
progressBar = navigationProgressBar;
|
typeName = tr("Navigation");
|
||||||
modelFrame = navigationFrame;
|
|
||||||
break;
|
break;
|
||||||
case cereal::ModelManagerSP::Model::Type::VISION:
|
case cereal::ModelManagerSP::Model::Type::VISION:
|
||||||
progressBar = visionProgressBar;
|
typeName = tr("Vision");
|
||||||
modelFrame = visionFrame;
|
|
||||||
break;
|
break;
|
||||||
case cereal::ModelManagerSP::Model::Type::POLICY:
|
case cereal::ModelManagerSP::Model::Type::POLICY:
|
||||||
progressBar = policyProgressBar;
|
typeName = tr("Policy");
|
||||||
modelFrame = policyFrame;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,26 +83,31 @@ void ModelsPanel::handleBundleDownloadProgress() {
|
|||||||
QString line;
|
QString line;
|
||||||
|
|
||||||
if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING) {
|
if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING) {
|
||||||
progressBar->setStyleSheet(progressStyleActive);
|
line = tr("Downloading %1 model [%2]... (%3%)").arg(typeName, modelName).arg(progress.getProgress(), 0, 'f', 2);
|
||||||
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) {
|
} else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADED) {
|
||||||
progressBar->setStyleSheet(progressStyleDone);
|
line = tr("%1 model [%2] %3").arg(typeName, modelName, download_status_changed ? tr("downloaded") : tr("ready"));
|
||||||
progressBar->setFormat(tr(" %1 - %2").arg(modelName, download_status_changed ? tr("downloaded") : tr("ready")));
|
|
||||||
} else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::CACHED) {
|
} else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::CACHED) {
|
||||||
progressBar->setStyleSheet(progressStyleDone);
|
line = tr("%1 model [%2] %3").arg(typeName, modelName, download_status_changed ? tr("from cache") : tr("ready"));
|
||||||
progressBar->setFormat(tr(" %1 - %2").arg(modelName, download_status_changed ? tr("from cache") : tr("ready")));
|
|
||||||
} else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::FAILED) {
|
} else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::FAILED) {
|
||||||
progressBar->setStyleSheet(progressStyleError);
|
line = tr("%1 model [%2] download failed").arg(typeName, modelName);
|
||||||
progressBar->setFormat(tr(" download failed - %1").arg(modelName));
|
|
||||||
} else {
|
} else {
|
||||||
progressBar->setStyleSheet(progressStyleInactive);
|
line = tr("%1 model [%2] pending...").arg(typeName, modelName);
|
||||||
progressBar->setFormat(tr(" pending - %1").arg(modelName));
|
|
||||||
}
|
}
|
||||||
// keep navigation hidden for now to avoid confusion
|
status.append(line);
|
||||||
if (model.getType() != cereal::ModelManagerSP::Model::Type::NAVIGATION) {
|
}
|
||||||
modelFrame->setVisible(true);
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prev_download_status = download_status;
|
prev_download_status = download_status;
|
||||||
@@ -206,17 +125,6 @@ QString ModelsPanel::GetActiveModelName() {
|
|||||||
return DEFAULT_MODEL;
|
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() {
|
void ModelsPanel::updateModelManagerState() {
|
||||||
const SubMaster &sm = *(uiStateSP()->sm);
|
const SubMaster &sm = *(uiStateSP()->sm);
|
||||||
model_manager = sm["modelManagerSP"].getModelManagerSP();
|
model_manager = sm["modelManagerSP"].getModelManagerSP();
|
||||||
@@ -248,7 +156,7 @@ void ModelsPanel::handleCurrentModelLblBtnClicked() {
|
|||||||
bundleNames.append(index_to_bundle[index]);
|
bundleNames.append(index_to_bundle[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentModelLblBtn->setValue(GetActiveModelInternalName());
|
currentModelLblBtn->setValue(GetActiveModelName());
|
||||||
|
|
||||||
const QString selectedBundleName = MultiOptionDialog::getSelection(
|
const QString selectedBundleName = MultiOptionDialog::getSelection(
|
||||||
tr("Select a Model"), bundleNames, GetActiveModelName(), this);
|
tr("Select a Model"), bundleNames, GetActiveModelName(), this);
|
||||||
@@ -289,7 +197,7 @@ void ModelsPanel::updateLabels() {
|
|||||||
updateModelManagerState();
|
updateModelManagerState();
|
||||||
handleBundleDownloadProgress();
|
handleBundleDownloadProgress();
|
||||||
currentModelLblBtn->setEnabled(!is_onroad && !isDownloading());
|
currentModelLblBtn->setEnabled(!is_onroad && !isDownloading());
|
||||||
currentModelLblBtn->setValue(GetActiveModelInternalName());
|
currentModelLblBtn->setValue(GetActiveModelName());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,8 +7,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QProgressBar>
|
|
||||||
|
|
||||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||||
|
|
||||||
class ModelsPanel : public QWidget {
|
class ModelsPanel : public QWidget {
|
||||||
@@ -19,7 +17,6 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
QString GetActiveModelName();
|
QString GetActiveModelName();
|
||||||
QString GetActiveModelInternalName();
|
|
||||||
void updateModelManagerState();
|
void updateModelManagerState();
|
||||||
|
|
||||||
bool isDownloading() const {
|
bool isDownloading() const {
|
||||||
@@ -36,8 +33,6 @@ private:
|
|||||||
void handleCurrentModelLblBtnClicked();
|
void handleCurrentModelLblBtnClicked();
|
||||||
void handleBundleDownloadProgress();
|
void handleBundleDownloadProgress();
|
||||||
void showResetParamsDialog();
|
void showResetParamsDialog();
|
||||||
QProgressBar* createProgressBar(QWidget *parent);
|
|
||||||
QFrame* createModelDetailFrame(QWidget *parent, QString &typeName, QProgressBar *progressBar);
|
|
||||||
cereal::ModelManagerSP::Reader model_manager;
|
cereal::ModelManagerSP::Reader model_manager;
|
||||||
cereal::ModelManagerSP::DownloadStatus download_status{};
|
cereal::ModelManagerSP::DownloadStatus download_status{};
|
||||||
cereal::ModelManagerSP::DownloadStatus prev_download_status{};
|
cereal::ModelManagerSP::DownloadStatus prev_download_status{};
|
||||||
@@ -64,14 +59,6 @@ private:
|
|||||||
bool is_onroad = false;
|
bool is_onroad = false;
|
||||||
|
|
||||||
ButtonControlSP *currentModelLblBtn;
|
ButtonControlSP *currentModelLblBtn;
|
||||||
QProgressBar *supercomboProgressBar;
|
|
||||||
QFrame *supercomboFrame;
|
|
||||||
QProgressBar *navigationProgressBar;
|
|
||||||
QFrame *navigationFrame;
|
|
||||||
QProgressBar *visionProgressBar;
|
|
||||||
QFrame *visionFrame;
|
|
||||||
QProgressBar *policyProgressBar;
|
|
||||||
QFrame *policyFrame;
|
|
||||||
Params params;
|
Params params;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) {
|
|||||||
|
|
||||||
TogglesPanelSP *toggles = new TogglesPanelSP(this);
|
TogglesPanelSP *toggles = new TogglesPanelSP(this);
|
||||||
QObject::connect(this, &SettingsWindowSP::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription);
|
QObject::connect(this, &SettingsWindowSP::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription);
|
||||||
QObject::connect(this, &SettingsWindowSP::scrollToToggle, toggles, &TogglesPanel::scrollToToggle);
|
|
||||||
|
|
||||||
auto networking = new NetworkingSP(this);
|
auto networking = new NetworkingSP(this);
|
||||||
QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &NetworkingSP::setPrimeType);
|
QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &NetworkingSP::setPrimeType);
|
||||||
|
|||||||
@@ -391,9 +391,9 @@ def create_screenshots():
|
|||||||
driver_img = frames[2]
|
driver_img = frames[2]
|
||||||
else:
|
else:
|
||||||
with open(frames_cache, 'wb') as f:
|
with open(frames_cache, 'wb') as f:
|
||||||
road_img = FrameReader(route.camera_paths()[segnum], pix_fmt="nv12").get(0)
|
road_img = FrameReader(route.camera_paths()[segnum]).get(0, pix_fmt="nv12")[0]
|
||||||
wide_road_img = FrameReader(route.ecamera_paths()[segnum], pix_fmt="nv12").get(0)
|
wide_road_img = FrameReader(route.ecamera_paths()[segnum]).get(0, pix_fmt="nv12")[0]
|
||||||
driver_img = FrameReader(route.dcamera_paths()[segnum], pix_fmt="nv12").get(0)
|
driver_img = FrameReader(route.dcamera_paths()[segnum]).get(0, pix_fmt="nv12")[0]
|
||||||
pickle.dump([road_img, wide_road_img, driver_img], f)
|
pickle.dump([road_img, wide_road_img, driver_img], f)
|
||||||
|
|
||||||
STREAMS.append((VisionStreamType.VISION_STREAM_ROAD, cam.fcam, road_img.flatten().tobytes()))
|
STREAMS.append((VisionStreamType.VISION_STREAM_ROAD, cam.fcam, road_img.flatten().tobytes()))
|
||||||
|
|||||||
+3
-3
@@ -6,16 +6,16 @@ from openpilot.selfdrive.ui.layouts.main import MainLayout
|
|||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
gui_app.init_window("UI")
|
gui_app.init_window("UI")
|
||||||
main_layout = MainLayout()
|
main_layout = MainLayout()
|
||||||
main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
|
||||||
for _ in gui_app.render():
|
for _ in gui_app.render():
|
||||||
ui_state.update()
|
ui_state.update()
|
||||||
|
|
||||||
# TODO handle brigntness and awake state here
|
#TODO handle brigntness and awake state here
|
||||||
|
|
||||||
main_layout.render()
|
main_layout.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||||
|
|
||||||
kick_watchdog()
|
kick_watchdog()
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import pyray as rl
|
import pyray as rl
|
||||||
import time
|
|
||||||
from collections.abc import Callable
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from cereal import messaging, log
|
from cereal import messaging, log
|
||||||
from openpilot.common.params import Params, UnknownKeyName
|
from openpilot.common.params import Params, UnknownKeyName
|
||||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeState
|
|
||||||
|
|
||||||
UI_BORDER_SIZE = 30
|
UI_BORDER_SIZE = 30
|
||||||
|
|
||||||
@@ -46,8 +44,6 @@ class UIState:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
self.prime_state = PrimeState()
|
|
||||||
|
|
||||||
# UI Status tracking
|
# UI Status tracking
|
||||||
self.status: UIStatus = UIStatus.DISENGAGED
|
self.status: UIStatus = UIStatus.DISENGAGED
|
||||||
self.started_frame: int = 0
|
self.started_frame: int = 0
|
||||||
@@ -68,17 +64,10 @@ class UIState:
|
|||||||
def engaged(self) -> bool:
|
def engaged(self) -> bool:
|
||||||
return self.started and self.sm["selfdriveState"].enabled
|
return self.started and self.sm["selfdriveState"].enabled
|
||||||
|
|
||||||
def is_onroad(self) -> bool:
|
|
||||||
return self.started
|
|
||||||
|
|
||||||
def is_offroad(self) -> bool:
|
|
||||||
return not self.started
|
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
self.sm.update(0)
|
self.sm.update(0)
|
||||||
self._update_state()
|
self._update_state()
|
||||||
self._update_status()
|
self._update_status()
|
||||||
device.update()
|
|
||||||
|
|
||||||
def _update_state(self) -> None:
|
def _update_state(self) -> None:
|
||||||
# Handle panda states updates
|
# Handle panda states updates
|
||||||
@@ -136,36 +125,5 @@ class UIState:
|
|||||||
self.is_metric = False
|
self.is_metric = False
|
||||||
|
|
||||||
|
|
||||||
class Device:
|
|
||||||
def __init__(self):
|
|
||||||
self._ignition = False
|
|
||||||
self._interaction_time: float = 0.0
|
|
||||||
self._interactive_timeout_callbacks: list[Callable] = []
|
|
||||||
self._prev_timed_out = False
|
|
||||||
self.reset_interactive_timeout()
|
|
||||||
|
|
||||||
def reset_interactive_timeout(self, timeout: int = -1) -> None:
|
|
||||||
if timeout == -1:
|
|
||||||
timeout = 10 if ui_state.ignition else 30
|
|
||||||
self._interaction_time = time.monotonic() + timeout
|
|
||||||
|
|
||||||
def add_interactive_timeout_callback(self, callback: Callable):
|
|
||||||
self._interactive_timeout_callbacks.append(callback)
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
# Handle interactive timeout
|
|
||||||
ignition_just_turned_off = not ui_state.ignition and self._ignition
|
|
||||||
self._ignition = ui_state.ignition
|
|
||||||
|
|
||||||
interaction_timeout = time.monotonic() > self._interaction_time
|
|
||||||
if ignition_just_turned_off or rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
|
||||||
self.reset_interactive_timeout()
|
|
||||||
elif interaction_timeout and not self._prev_timed_out:
|
|
||||||
for callback in self._interactive_timeout_callbacks:
|
|
||||||
callback()
|
|
||||||
self._prev_timed_out = interaction_timeout
|
|
||||||
|
|
||||||
|
|
||||||
# Global instance
|
# Global instance
|
||||||
ui_state = UIState()
|
ui_state = UIState()
|
||||||
device = Device()
|
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
import pyray as rl
|
|
||||||
from openpilot.common.params import Params
|
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
|
||||||
from openpilot.system.ui.lib.widget import Widget
|
|
||||||
|
|
||||||
|
|
||||||
class ExperimentalModeButton(Widget):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.img_width = 80
|
|
||||||
self.horizontal_padding = 50
|
|
||||||
self.button_height = 125
|
|
||||||
|
|
||||||
self.params = Params()
|
|
||||||
self.experimental_mode = self.params.get_bool("ExperimentalMode")
|
|
||||||
self.is_pressed = False
|
|
||||||
|
|
||||||
self.chill_pixmap = gui_app.texture("icons/couch.png", self.img_width, self.img_width)
|
|
||||||
self.experimental_pixmap = gui_app.texture("icons/experimental_grey.png", self.img_width, self.img_width)
|
|
||||||
|
|
||||||
def _get_gradient_colors(self):
|
|
||||||
alpha = 0xCC if self.is_pressed else 0xFF
|
|
||||||
|
|
||||||
if self.experimental_mode:
|
|
||||||
return rl.Color(255, 155, 63, alpha), rl.Color(219, 56, 34, alpha)
|
|
||||||
else:
|
|
||||||
return rl.Color(20, 255, 171, alpha), rl.Color(35, 149, 255, alpha)
|
|
||||||
|
|
||||||
def _draw_gradient_background(self, rect):
|
|
||||||
start_color, end_color = self._get_gradient_colors()
|
|
||||||
rl.draw_rectangle_gradient_h(int(rect.x), int(rect.y), int(rect.width), int(rect.height),
|
|
||||||
start_color, end_color)
|
|
||||||
|
|
||||||
def _handle_interaction(self, rect):
|
|
||||||
mouse_pos = rl.get_mouse_position()
|
|
||||||
mouse_in_rect = rl.check_collision_point_rec(mouse_pos, rect)
|
|
||||||
|
|
||||||
self.is_pressed = mouse_in_rect and rl.is_mouse_button_down(rl.MOUSE_BUTTON_LEFT)
|
|
||||||
return mouse_in_rect and rl.is_mouse_button_released(rl.MOUSE_BUTTON_LEFT)
|
|
||||||
|
|
||||||
def _render(self, rect):
|
|
||||||
if self._handle_interaction(rect):
|
|
||||||
self.experimental_mode = not self.experimental_mode
|
|
||||||
# TODO: Opening settings for ExperimentalMode
|
|
||||||
self.params.put_bool("ExperimentalMode", self.experimental_mode)
|
|
||||||
|
|
||||||
rl.draw_rectangle_rounded(rect, 0.08, 20, rl.Color(255, 255, 255, 255))
|
|
||||||
|
|
||||||
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
|
||||||
self._draw_gradient_background(rect)
|
|
||||||
rl.end_scissor_mode()
|
|
||||||
|
|
||||||
# Draw vertical separator line
|
|
||||||
line_x = rect.x + rect.width - self.img_width - (2 * self.horizontal_padding)
|
|
||||||
separator_color = rl.Color(0, 0, 0, 77) # 0x4d = 77
|
|
||||||
rl.draw_line_ex(rl.Vector2(line_x, rect.y), rl.Vector2(line_x, rect.y + rect.height), 3, separator_color)
|
|
||||||
|
|
||||||
# Draw text label (left aligned)
|
|
||||||
text = "EXPERIMENTAL MODE ON" if self.experimental_mode else "CHILL MODE ON"
|
|
||||||
text_x = rect.x + self.horizontal_padding
|
|
||||||
text_y = rect.y + rect.height / 2 - 45 // 2 # Center vertically
|
|
||||||
|
|
||||||
rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), text, rl.Vector2(int(text_x), int(text_y)), 45, 0, rl.Color(0, 0, 0, 255))
|
|
||||||
|
|
||||||
# Draw icon (right aligned)
|
|
||||||
icon_x = rect.x + rect.width - self.horizontal_padding - self.img_width
|
|
||||||
icon_y = rect.y + (rect.height - self.img_width) / 2
|
|
||||||
icon_rect = rl.Rectangle(icon_x, icon_y, self.img_width, self.img_width)
|
|
||||||
|
|
||||||
# Draw current mode icon
|
|
||||||
current_icon = self.experimental_pixmap if self.experimental_mode else self.chill_pixmap
|
|
||||||
source_rect = rl.Rectangle(0, 0, current_icon.width, current_icon.height)
|
|
||||||
rl.draw_texture_pro(current_icon, source_rect, icon_rect, rl.Vector2(0, 0), 0, rl.Color(255, 255, 255, 255))
|
|
||||||
@@ -9,8 +9,6 @@ from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
|||||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.widget import Widget
|
|
||||||
|
|
||||||
|
|
||||||
class AlertColors:
|
class AlertColors:
|
||||||
HIGH_SEVERITY = rl.Color(226, 44, 44, 255)
|
HIGH_SEVERITY = rl.Color(226, 44, 44, 255)
|
||||||
@@ -42,9 +40,8 @@ class AlertData:
|
|||||||
visible: bool = False
|
visible: bool = False
|
||||||
|
|
||||||
|
|
||||||
class AbstractAlert(Widget, ABC):
|
class AbstractAlert(ABC):
|
||||||
def __init__(self, has_reboot_btn: bool = False):
|
def __init__(self, has_reboot_btn: bool = False):
|
||||||
super().__init__()
|
|
||||||
self.params = Params()
|
self.params = Params()
|
||||||
self.has_reboot_btn = has_reboot_btn
|
self.has_reboot_btn = has_reboot_btn
|
||||||
self.dismiss_callback: Callable | None = None
|
self.dismiss_callback: Callable | None = None
|
||||||
@@ -70,7 +67,8 @@ class AbstractAlert(Widget, ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def handle_input(self, mouse_pos: rl.Vector2, mouse_clicked: bool) -> bool:
|
def handle_input(self, mouse_pos: rl.Vector2, mouse_clicked: bool) -> bool:
|
||||||
if not mouse_clicked or not self.scroll_panel.is_touch_valid():
|
# TODO: fix scroll_panel.is_click_valid()
|
||||||
|
if not mouse_clicked:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if rl.check_collision_point_rec(mouse_pos, self.dismiss_btn_rect):
|
if rl.check_collision_point_rec(mouse_pos, self.dismiss_btn_rect):
|
||||||
@@ -90,7 +88,7 @@ class AbstractAlert(Widget, ABC):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle):
|
def render(self, rect: rl.Rectangle):
|
||||||
rl.draw_rectangle_rounded(rect, AlertConstants.BORDER_RADIUS / rect.width, 10, AlertColors.BACKGROUND)
|
rl.draw_rectangle_rounded(rect, AlertConstants.BORDER_RADIUS / rect.width, 10, AlertColors.BACKGROUND)
|
||||||
|
|
||||||
footer_height = AlertConstants.BUTTON_SIZE[1] + AlertConstants.SPACING
|
footer_height = AlertConstants.BUTTON_SIZE[1] + AlertConstants.SPACING
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
import pyray as rl
|
|
||||||
import qrcode
|
|
||||||
import numpy as np
|
|
||||||
import time
|
|
||||||
|
|
||||||
from openpilot.common.api import Api
|
|
||||||
from openpilot.common.swaglog import cloudlog
|
|
||||||
from openpilot.common.params import Params
|
|
||||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
|
||||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
|
||||||
|
|
||||||
|
|
||||||
class PairingDialog:
|
|
||||||
"""Dialog for device pairing with QR code."""
|
|
||||||
|
|
||||||
QR_REFRESH_INTERVAL = 300 # 5 minutes in seconds
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.params = Params()
|
|
||||||
self.qr_texture: rl.Texture | None = None
|
|
||||||
self.last_qr_generation = 0
|
|
||||||
|
|
||||||
def _get_pairing_url(self) -> str:
|
|
||||||
try:
|
|
||||||
dongle_id = self.params.get("DongleId", encoding='utf8') or ""
|
|
||||||
token = Api(dongle_id).get_token()
|
|
||||||
except Exception as e:
|
|
||||||
cloudlog.warning(f"Failed to get pairing token: {e}")
|
|
||||||
token = ""
|
|
||||||
return f"https://connect.comma.ai/setup?token={token}"
|
|
||||||
|
|
||||||
def _generate_qr_code(self) -> None:
|
|
||||||
try:
|
|
||||||
qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=4)
|
|
||||||
qr.add_data(self._get_pairing_url())
|
|
||||||
qr.make(fit=True)
|
|
||||||
|
|
||||||
pil_img = qr.make_image(fill_color="black", back_color="white").convert('RGBA')
|
|
||||||
img_array = np.array(pil_img, dtype=np.uint8)
|
|
||||||
|
|
||||||
if self.qr_texture and self.qr_texture.id != 0:
|
|
||||||
rl.unload_texture(self.qr_texture)
|
|
||||||
|
|
||||||
rl_image = rl.Image()
|
|
||||||
rl_image.data = rl.ffi.cast("void *", img_array.ctypes.data)
|
|
||||||
rl_image.width = pil_img.width
|
|
||||||
rl_image.height = pil_img.height
|
|
||||||
rl_image.mipmaps = 1
|
|
||||||
rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
|
|
||||||
|
|
||||||
self.qr_texture = rl.load_texture_from_image(rl_image)
|
|
||||||
except Exception as e:
|
|
||||||
cloudlog.warning(f"QR code generation failed: {e}")
|
|
||||||
self.qr_texture = None
|
|
||||||
|
|
||||||
def _check_qr_refresh(self) -> None:
|
|
||||||
current_time = time.time()
|
|
||||||
if current_time - self.last_qr_generation >= self.QR_REFRESH_INTERVAL:
|
|
||||||
self._generate_qr_code()
|
|
||||||
self.last_qr_generation = current_time
|
|
||||||
|
|
||||||
def render(self, rect: rl.Rectangle) -> int:
|
|
||||||
rl.clear_background(rl.Color(224, 224, 224, 255))
|
|
||||||
|
|
||||||
self._check_qr_refresh()
|
|
||||||
|
|
||||||
margin = 70
|
|
||||||
content_rect = rl.Rectangle(rect.x + margin, rect.y + margin, rect.width - 2 * margin, rect.height - 2 * margin)
|
|
||||||
y = content_rect.y
|
|
||||||
|
|
||||||
# Close button
|
|
||||||
close_size = 80
|
|
||||||
close_icon = gui_app.texture("icons/close.png", close_size, close_size)
|
|
||||||
close_rect = rl.Rectangle(content_rect.x, y, close_size, close_size)
|
|
||||||
|
|
||||||
mouse_pos = rl.get_mouse_position()
|
|
||||||
is_hover = rl.check_collision_point_rec(mouse_pos, close_rect)
|
|
||||||
is_pressed = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
|
||||||
is_released = rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
|
||||||
|
|
||||||
color = rl.Color(180, 180, 180, 150) if (is_hover and is_pressed) else rl.WHITE
|
|
||||||
rl.draw_texture(close_icon, int(content_rect.x), int(y), color)
|
|
||||||
|
|
||||||
if (is_hover and is_released) or rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
|
|
||||||
return 1
|
|
||||||
|
|
||||||
y += close_size + 40
|
|
||||||
|
|
||||||
# Title
|
|
||||||
title = "Pair your device to your comma account"
|
|
||||||
title_font = gui_app.font(FontWeight.NORMAL)
|
|
||||||
left_width = int(content_rect.width * 0.5 - 15)
|
|
||||||
|
|
||||||
title_wrapped = wrap_text(title_font, title, 75, left_width)
|
|
||||||
rl.draw_text_ex(title_font, "\n".join(title_wrapped), rl.Vector2(content_rect.x, y), 75, 0.0, rl.BLACK)
|
|
||||||
y += len(title_wrapped) * 75 + 60
|
|
||||||
|
|
||||||
# Two columns: instructions and QR code
|
|
||||||
remaining_height = content_rect.height - (y - content_rect.y)
|
|
||||||
right_width = content_rect.width // 2 - 20
|
|
||||||
|
|
||||||
# Instructions
|
|
||||||
self._render_instructions(rl.Rectangle(content_rect.x, y, left_width, remaining_height))
|
|
||||||
|
|
||||||
# QR code
|
|
||||||
qr_size = min(right_width, content_rect.height) - 40
|
|
||||||
qr_x = content_rect.x + left_width + 40 + (right_width - qr_size) // 2
|
|
||||||
qr_y = content_rect.y
|
|
||||||
self._render_qr_code(rl.Rectangle(qr_x, qr_y, qr_size, qr_size))
|
|
||||||
|
|
||||||
return -1
|
|
||||||
|
|
||||||
def _render_instructions(self, rect: rl.Rectangle) -> None:
|
|
||||||
instructions = [
|
|
||||||
"Go to https://connect.comma.ai on your phone",
|
|
||||||
"Click \"add new device\" and scan the QR code on the right",
|
|
||||||
"Bookmark connect.comma.ai to your home screen to use it like an app",
|
|
||||||
]
|
|
||||||
|
|
||||||
font = gui_app.font(FontWeight.BOLD)
|
|
||||||
y = rect.y
|
|
||||||
|
|
||||||
for i, text in enumerate(instructions):
|
|
||||||
circle_radius = 25
|
|
||||||
circle_x = rect.x + circle_radius + 15
|
|
||||||
text_x = rect.x + circle_radius * 2 + 40
|
|
||||||
text_width = rect.width - (circle_radius * 2 + 40)
|
|
||||||
|
|
||||||
wrapped = wrap_text(font, text, 47, int(text_width))
|
|
||||||
text_height = len(wrapped) * 47
|
|
||||||
circle_y = y + text_height // 2
|
|
||||||
|
|
||||||
# Circle and number
|
|
||||||
rl.draw_circle(int(circle_x), int(circle_y), circle_radius, rl.Color(70, 70, 70, 255))
|
|
||||||
number = str(i + 1)
|
|
||||||
number_width = measure_text_cached(font, number, 30).x
|
|
||||||
rl.draw_text(number, int(circle_x - number_width // 2), int(circle_y - 15), 30, rl.WHITE)
|
|
||||||
|
|
||||||
# Text
|
|
||||||
rl.draw_text_ex(font, "\n".join(wrapped), rl.Vector2(text_x, y), 47, 0.0, rl.BLACK)
|
|
||||||
y += text_height + 50
|
|
||||||
|
|
||||||
def _render_qr_code(self, rect: rl.Rectangle) -> None:
|
|
||||||
if not self.qr_texture:
|
|
||||||
rl.draw_rectangle_rounded(rect, 0.1, 20, rl.Color(240, 240, 240, 255))
|
|
||||||
error_font = gui_app.font(FontWeight.BOLD)
|
|
||||||
rl.draw_text_ex(
|
|
||||||
error_font, "QR Code Error", rl.Vector2(rect.x + 20, rect.y + rect.height // 2 - 15), 30, 0.0, rl.RED
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
source = rl.Rectangle(0, 0, self.qr_texture.width, self.qr_texture.height)
|
|
||||||
rl.draw_texture_pro(self.qr_texture, source, rect, rl.Vector2(0, 0), 0, rl.WHITE)
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
if self.qr_texture and self.qr_texture.id != 0:
|
|
||||||
rl.unload_texture(self.qr_texture)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
gui_app.init_window("pairing device")
|
|
||||||
pairing = PairingDialog()
|
|
||||||
try:
|
|
||||||
for _ in gui_app.render():
|
|
||||||
result = pairing.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
|
||||||
if result != -1:
|
|
||||||
break
|
|
||||||
finally:
|
|
||||||
del pairing
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import pyray as rl
|
|
||||||
|
|
||||||
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
|
|
||||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
|
||||||
from openpilot.system.ui.lib.widget import Widget
|
|
||||||
|
|
||||||
|
|
||||||
class PrimeWidget(Widget):
|
|
||||||
"""Widget for displaying comma prime subscription status"""
|
|
||||||
|
|
||||||
PRIME_BG_COLOR = rl.Color(51, 51, 51, 255)
|
|
||||||
|
|
||||||
def _render(self, rect):
|
|
||||||
if ui_state.prime_state.is_prime():
|
|
||||||
self._render_for_prime_user(rect)
|
|
||||||
else:
|
|
||||||
self._render_for_non_prime_users(rect)
|
|
||||||
|
|
||||||
def _render_for_non_prime_users(self, rect: rl.Rectangle):
|
|
||||||
"""Renders the advertisement for non-Prime users."""
|
|
||||||
|
|
||||||
rl.draw_rectangle_rounded(rect, 0.02, 10, self.PRIME_BG_COLOR)
|
|
||||||
|
|
||||||
# Layout
|
|
||||||
x, y = rect.x + 80, rect.y + 90
|
|
||||||
w = rect.width - 160
|
|
||||||
|
|
||||||
# Title
|
|
||||||
gui_label(rl.Rectangle(x, y, w, 90), "Upgrade Now", 75, font_weight=FontWeight.BOLD)
|
|
||||||
|
|
||||||
# Description with wrapping
|
|
||||||
desc_y = y + 140
|
|
||||||
font = gui_app.font(FontWeight.LIGHT)
|
|
||||||
wrapped_text = "\n".join(wrap_text(font, "Become a comma prime member at connect.comma.ai", 56, int(w)))
|
|
||||||
text_size = measure_text_cached(font, wrapped_text, 56)
|
|
||||||
rl.draw_text_ex(font, wrapped_text, rl.Vector2(x, desc_y), 56, 0, rl.Color(255, 255, 255, 255))
|
|
||||||
|
|
||||||
# Features section
|
|
||||||
features_y = desc_y + text_size.y + 50
|
|
||||||
gui_label(rl.Rectangle(x, features_y, w, 50), "PRIME FEATURES:", 41, font_weight=FontWeight.BOLD)
|
|
||||||
|
|
||||||
# Feature list
|
|
||||||
features = ["Remote access", "24/7 LTE connectivity", "1 year of drive storage", "Remote snapshots"]
|
|
||||||
for i, feature in enumerate(features):
|
|
||||||
item_y = features_y + 80 + i * 65
|
|
||||||
gui_label(rl.Rectangle(x, item_y, 50, 60), "✓", 50, color=rl.Color(70, 91, 234, 255))
|
|
||||||
gui_label(rl.Rectangle(x + 60, item_y, w - 60, 60), feature, 50)
|
|
||||||
|
|
||||||
def _render_for_prime_user(self, rect: rl.Rectangle):
|
|
||||||
"""Renders the prime user widget with subscription status."""
|
|
||||||
|
|
||||||
rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 230), 0.02, 10, self.PRIME_BG_COLOR)
|
|
||||||
|
|
||||||
x = rect.x + 56
|
|
||||||
y = rect.y + 40
|
|
||||||
|
|
||||||
font = gui_app.font(FontWeight.BOLD)
|
|
||||||
rl.draw_text_ex(font, "✓ SUBSCRIBED", rl.Vector2(x, y), 41, 0, rl.Color(134, 255, 78, 255))
|
|
||||||
rl.draw_text_ex(font, "comma prime", rl.Vector2(x, y + 61), 75, 0, rl.WHITE)
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import pyray as rl
|
|
||||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
|
|
||||||
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
|
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
|
||||||
from openpilot.system.ui.lib.button import gui_button, ButtonStyle
|
|
||||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
|
||||||
from openpilot.system.ui.lib.widget import Widget
|
|
||||||
|
|
||||||
|
|
||||||
class SetupWidget(Widget):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self._open_settings_callback = None
|
|
||||||
self._pairing_dialog: PairingDialog | None = None
|
|
||||||
|
|
||||||
def set_open_settings_callback(self, callback):
|
|
||||||
self._open_settings_callback = callback
|
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle):
|
|
||||||
if ui_state.prime_state.get_type() == PrimeType.UNPAIRED:
|
|
||||||
self._render_registration(rect)
|
|
||||||
else:
|
|
||||||
self._render_firehose_prompt(rect)
|
|
||||||
|
|
||||||
def _render_registration(self, rect: rl.Rectangle):
|
|
||||||
"""Render registration prompt."""
|
|
||||||
|
|
||||||
rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 590), 0.02, 20, rl.Color(51, 51, 51, 255))
|
|
||||||
|
|
||||||
x = rect.x + 64
|
|
||||||
y = rect.y + 48
|
|
||||||
w = rect.width - 128
|
|
||||||
|
|
||||||
# Title
|
|
||||||
font = gui_app.font(FontWeight.BOLD)
|
|
||||||
rl.draw_text_ex(font, "Finish Setup", rl.Vector2(x, y), 75, 0, rl.WHITE)
|
|
||||||
y += 113 # 75 + 38 spacing
|
|
||||||
|
|
||||||
# Description
|
|
||||||
desc = "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer."
|
|
||||||
light_font = gui_app.font(FontWeight.LIGHT)
|
|
||||||
wrapped = wrap_text(light_font, desc, 50, int(w))
|
|
||||||
for line in wrapped:
|
|
||||||
rl.draw_text_ex(light_font, line, rl.Vector2(x, y), 50, 0, rl.WHITE)
|
|
||||||
y += 50
|
|
||||||
|
|
||||||
button_rect = rl.Rectangle(x, y + 50, w, 128)
|
|
||||||
if gui_button(button_rect, "Pair device", button_style=ButtonStyle.PRIMARY):
|
|
||||||
self._show_pairing()
|
|
||||||
|
|
||||||
def _render_firehose_prompt(self, rect: rl.Rectangle):
|
|
||||||
"""Render firehose prompt widget."""
|
|
||||||
|
|
||||||
rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 450), 0.02, 20, rl.Color(51, 51, 51, 255))
|
|
||||||
|
|
||||||
# Content margins (56, 40, 56, 40)
|
|
||||||
x = rect.x + 56
|
|
||||||
y = rect.y + 40
|
|
||||||
w = rect.width - 112
|
|
||||||
spacing = 42
|
|
||||||
|
|
||||||
# Title with fire emojis
|
|
||||||
title_font = gui_app.font(FontWeight.MEDIUM)
|
|
||||||
title_text = "Firehose Mode"
|
|
||||||
rl.draw_text_ex(title_font, title_text, rl.Vector2(x, y), 64, 0, rl.WHITE)
|
|
||||||
y += 64 + spacing
|
|
||||||
|
|
||||||
# Description
|
|
||||||
desc_font = gui_app.font(FontWeight.NORMAL)
|
|
||||||
desc_text = "Maximize your training data uploads to improve openpilot's driving models."
|
|
||||||
wrapped_desc = wrap_text(desc_font, desc_text, 40, int(w))
|
|
||||||
|
|
||||||
for line in wrapped_desc:
|
|
||||||
rl.draw_text_ex(desc_font, line, rl.Vector2(x, y), 40, 0, rl.WHITE)
|
|
||||||
y += 40
|
|
||||||
|
|
||||||
y += spacing
|
|
||||||
|
|
||||||
# Open button
|
|
||||||
button_height = 48 + 64 # font size + padding
|
|
||||||
button_rect = rl.Rectangle(x, y, w, button_height)
|
|
||||||
if gui_button(button_rect, "Open", button_style=ButtonStyle.PRIMARY):
|
|
||||||
if self._open_settings_callback:
|
|
||||||
self._open_settings_callback()
|
|
||||||
|
|
||||||
def _show_pairing(self):
|
|
||||||
if not self._pairing_dialog:
|
|
||||||
self._pairing_dialog = PairingDialog()
|
|
||||||
gui_app.set_modal_overlay(self._pairing_dialog, lambda result: setattr(self, '_pairing_dialog', None))
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
if self._pairing_dialog:
|
|
||||||
del self._pairing_dialog
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import pyray as rl
|
|
||||||
import requests
|
|
||||||
import threading
|
|
||||||
import copy
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from openpilot.common.params import Params
|
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
|
||||||
from openpilot.system.ui.lib.button import gui_button, ButtonStyle
|
|
||||||
from openpilot.system.ui.lib.list_view import (
|
|
||||||
ItemAction,
|
|
||||||
ListItem,
|
|
||||||
BUTTON_HEIGHT,
|
|
||||||
BUTTON_BORDER_RADIUS,
|
|
||||||
BUTTON_FONT_SIZE,
|
|
||||||
BUTTON_WIDTH,
|
|
||||||
)
|
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
|
||||||
from openpilot.system.ui.lib.widget import DialogResult
|
|
||||||
from openpilot.system.ui.widgets.confirm_dialog import alert_dialog
|
|
||||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
|
||||||
|
|
||||||
|
|
||||||
class SshKeyActionState(Enum):
|
|
||||||
LOADING = "LOADING"
|
|
||||||
ADD = "ADD"
|
|
||||||
REMOVE = "REMOVE"
|
|
||||||
|
|
||||||
|
|
||||||
class SshKeyAction(ItemAction):
|
|
||||||
HTTP_TIMEOUT = 15 # seconds
|
|
||||||
MAX_WIDTH = 500
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(self.MAX_WIDTH, True)
|
|
||||||
|
|
||||||
self._keyboard = Keyboard()
|
|
||||||
self._params = Params()
|
|
||||||
self._error_message: str = ""
|
|
||||||
self._text_font = gui_app.font(FontWeight.MEDIUM)
|
|
||||||
|
|
||||||
self._refresh_state()
|
|
||||||
|
|
||||||
def _refresh_state(self):
|
|
||||||
self._username = self._params.get("GithubUsername", "")
|
|
||||||
self._state = SshKeyActionState.REMOVE if self._params.get("GithubSshKeys") else SshKeyActionState.ADD
|
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle) -> bool:
|
|
||||||
# Show error dialog if there's an error
|
|
||||||
if self._error_message:
|
|
||||||
message = copy.copy(self._error_message)
|
|
||||||
gui_app.set_modal_overlay(lambda: alert_dialog(message))
|
|
||||||
self._username = ""
|
|
||||||
self._error_message = ""
|
|
||||||
|
|
||||||
# Draw username if exists
|
|
||||||
if self._username:
|
|
||||||
text_size = measure_text_cached(self._text_font, self._username, BUTTON_FONT_SIZE)
|
|
||||||
rl.draw_text_ex(
|
|
||||||
self._text_font,
|
|
||||||
self._username,
|
|
||||||
(rect.x + rect.width - BUTTON_WIDTH - text_size.x - 30, rect.y + (rect.height - text_size.y) / 2),
|
|
||||||
BUTTON_FONT_SIZE,
|
|
||||||
1.0,
|
|
||||||
rl.WHITE,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Draw button
|
|
||||||
if gui_button(
|
|
||||||
rl.Rectangle(
|
|
||||||
rect.x + rect.width - BUTTON_WIDTH, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT
|
|
||||||
),
|
|
||||||
self._state.value,
|
|
||||||
is_enabled=self._state != SshKeyActionState.LOADING,
|
|
||||||
border_radius=BUTTON_BORDER_RADIUS,
|
|
||||||
font_size=BUTTON_FONT_SIZE,
|
|
||||||
button_style=ButtonStyle.LIST_ACTION,
|
|
||||||
):
|
|
||||||
self._handle_button_click()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _handle_button_click(self):
|
|
||||||
if self._state == SshKeyActionState.ADD:
|
|
||||||
self._keyboard.clear()
|
|
||||||
self._keyboard.set_title("Enter your GitHub username")
|
|
||||||
gui_app.set_modal_overlay(self._keyboard, callback=self._on_username_submit)
|
|
||||||
elif self._state == SshKeyActionState.REMOVE:
|
|
||||||
self._params.remove("GithubUsername")
|
|
||||||
self._params.remove("GithubSshKeys")
|
|
||||||
self._refresh_state()
|
|
||||||
|
|
||||||
def _on_username_submit(self, result: DialogResult):
|
|
||||||
if result != DialogResult.CONFIRM:
|
|
||||||
return
|
|
||||||
|
|
||||||
username = self._keyboard.text.strip()
|
|
||||||
if not username:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._state = SshKeyActionState.LOADING
|
|
||||||
threading.Thread(target=lambda: self._fetch_ssh_key(username), daemon=True).start()
|
|
||||||
|
|
||||||
def _fetch_ssh_key(self, username: str):
|
|
||||||
try:
|
|
||||||
url = f"https://github.com/{username}.keys"
|
|
||||||
response = requests.get(url, timeout=self.HTTP_TIMEOUT)
|
|
||||||
response.raise_for_status()
|
|
||||||
keys = response.text.strip()
|
|
||||||
if not keys:
|
|
||||||
raise requests.exceptions.HTTPError("No SSH keys found")
|
|
||||||
|
|
||||||
# Success - save keys
|
|
||||||
self._params.put("GithubUsername", username)
|
|
||||||
self._params.put("GithubSshKeys", keys)
|
|
||||||
self._state = SshKeyActionState.REMOVE
|
|
||||||
self._username = username
|
|
||||||
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
self._error_message = "Request timed out"
|
|
||||||
self._state = SshKeyActionState.ADD
|
|
||||||
except Exception:
|
|
||||||
self._error_message = f"No SSH keys found for user '{username}'"
|
|
||||||
self._state = SshKeyActionState.ADD
|
|
||||||
|
|
||||||
|
|
||||||
def ssh_key_item(title: str, description: str):
|
|
||||||
return ListItem(title=title, description=description, action_item=SshKeyAction())
|
|
||||||
@@ -18,7 +18,7 @@ from cereal import messaging
|
|||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.sunnypilot.mapd.mapd_manager import MAPD_PATH, MAPD_BIN_DIR
|
from openpilot.sunnypilot.mapd.mapd_manager import MAPD_PATH, MAPD_BIN_DIR
|
||||||
from openpilot.system.hardware.hw import Paths
|
from openpilot.system.hardware.hw import Paths
|
||||||
from openpilot.common.spinner import Spinner
|
from openpilot.system.ui.spinner import Spinner
|
||||||
from openpilot.system.version import is_prebuilt
|
from openpilot.system.version import is_prebuilt
|
||||||
import openpilot.system.sentry as sentry
|
import openpilot.system.sentry as sentry
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
71979b29c4bab3007de1a4265442d79f44c0eaef066af66086dddfc432709b94
|
dec34c57c4131f6fca5d1035201d1afbf43e5250cede7bfdc798371af008afad
|
||||||
@@ -7,7 +7,6 @@ from pathlib import Path
|
|||||||
from datetime import datetime, timedelta, UTC
|
from datetime import datetime, timedelta, UTC
|
||||||
from openpilot.common.api import api_get
|
from openpilot.common.api import api_get
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.common.spinner import Spinner
|
|
||||||
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
||||||
from openpilot.system.hardware import HARDWARE, PC
|
from openpilot.system.hardware import HARDWARE, PC
|
||||||
from openpilot.system.hardware.hw import Paths
|
from openpilot.system.hardware.hw import Paths
|
||||||
@@ -45,6 +44,7 @@ def register(show_spinner=False) -> str | None:
|
|||||||
cloudlog.warning(f"missing public key: {pubkey}")
|
cloudlog.warning(f"missing public key: {pubkey}")
|
||||||
elif dongle_id is None:
|
elif dongle_id is None:
|
||||||
if show_spinner:
|
if show_spinner:
|
||||||
|
from openpilot.system.ui.spinner import Spinner
|
||||||
spinner = Spinner()
|
spinner = Spinner()
|
||||||
spinner.update("registering device")
|
spinner.update("registering device")
|
||||||
|
|
||||||
|
|||||||
@@ -56,28 +56,28 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "boot",
|
"name": "boot",
|
||||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef.img.xz",
|
"url": "https://commadist.azureedge.net/agnosupdate/boot-4143170bad94968fd9be870b1498b4100bf273ed0aec2a2601c9017991d4bd42.img.xz",
|
||||||
"hash": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef",
|
"hash": "4143170bad94968fd9be870b1498b4100bf273ed0aec2a2601c9017991d4bd42",
|
||||||
"hash_raw": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef",
|
"hash_raw": "4143170bad94968fd9be870b1498b4100bf273ed0aec2a2601c9017991d4bd42",
|
||||||
"size": 18479104,
|
"size": 18479104,
|
||||||
"sparse": false,
|
"sparse": false,
|
||||||
"full_check": true,
|
"full_check": true,
|
||||||
"has_ab": true,
|
"has_ab": true,
|
||||||
"ondevice_hash": "8d7094d774faa4e801e36b403a31b53b913b31d086f4dc682d2f64710c557e8a"
|
"ondevice_hash": "6b7b3371100ad36d8a5a9ff19a1663b9b9e2d5e99cbe3cf9255e9c3017291ce3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "system",
|
"name": "system",
|
||||||
"url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img.xz",
|
"url": "https://commadist.azureedge.net/agnosupdate/system-c51bb5841011728f7cf108a9138ba68228ffb4232dfd91d6e082a6d8a6a8deaa.img.xz",
|
||||||
"hash": "cccd7073d067027396f2afd49874729757db0bbbc79853a0bf2938bd356fe164",
|
"hash": "993d6a1cd2b684e2b1cf6ff840f8996f02a529011372d9c1471e4c80719e7da9",
|
||||||
"hash_raw": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39",
|
"hash_raw": "c51bb5841011728f7cf108a9138ba68228ffb4232dfd91d6e082a6d8a6a8deaa",
|
||||||
"size": 5368709120,
|
"size": 5368709120,
|
||||||
"sparse": true,
|
"sparse": true,
|
||||||
"full_check": false,
|
"full_check": false,
|
||||||
"has_ab": true,
|
"has_ab": true,
|
||||||
"ondevice_hash": "c7707f16ce7d977748677cc354e250943b4ff6c21b9a19a492053d32397cf9ec",
|
"ondevice_hash": "59db25651da977eeb16a1af741fd01fc3d6b50d21544b1a7428b7c86b2cdef2d",
|
||||||
"alt": {
|
"alt": {
|
||||||
"hash": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39",
|
"hash": "c51bb5841011728f7cf108a9138ba68228ffb4232dfd91d6e082a6d8a6a8deaa",
|
||||||
"url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img",
|
"url": "https://commadist.azureedge.net/agnosupdate/system-c51bb5841011728f7cf108a9138ba68228ffb4232dfd91d6e082a6d8a6a8deaa.img",
|
||||||
"size": 5368709120
|
"size": 5368709120
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -339,62 +339,62 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "boot",
|
"name": "boot",
|
||||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef.img.xz",
|
"url": "https://commadist.azureedge.net/agnosupdate/boot-4143170bad94968fd9be870b1498b4100bf273ed0aec2a2601c9017991d4bd42.img.xz",
|
||||||
"hash": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef",
|
"hash": "4143170bad94968fd9be870b1498b4100bf273ed0aec2a2601c9017991d4bd42",
|
||||||
"hash_raw": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef",
|
"hash_raw": "4143170bad94968fd9be870b1498b4100bf273ed0aec2a2601c9017991d4bd42",
|
||||||
"size": 18479104,
|
"size": 18479104,
|
||||||
"sparse": false,
|
"sparse": false,
|
||||||
"full_check": true,
|
"full_check": true,
|
||||||
"has_ab": true,
|
"has_ab": true,
|
||||||
"ondevice_hash": "8d7094d774faa4e801e36b403a31b53b913b31d086f4dc682d2f64710c557e8a"
|
"ondevice_hash": "6b7b3371100ad36d8a5a9ff19a1663b9b9e2d5e99cbe3cf9255e9c3017291ce3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "system",
|
"name": "system",
|
||||||
"url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img.xz",
|
"url": "https://commadist.azureedge.net/agnosupdate/system-c51bb5841011728f7cf108a9138ba68228ffb4232dfd91d6e082a6d8a6a8deaa.img.xz",
|
||||||
"hash": "cccd7073d067027396f2afd49874729757db0bbbc79853a0bf2938bd356fe164",
|
"hash": "993d6a1cd2b684e2b1cf6ff840f8996f02a529011372d9c1471e4c80719e7da9",
|
||||||
"hash_raw": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39",
|
"hash_raw": "c51bb5841011728f7cf108a9138ba68228ffb4232dfd91d6e082a6d8a6a8deaa",
|
||||||
"size": 5368709120,
|
"size": 5368709120,
|
||||||
"sparse": true,
|
"sparse": true,
|
||||||
"full_check": false,
|
"full_check": false,
|
||||||
"has_ab": true,
|
"has_ab": true,
|
||||||
"ondevice_hash": "c7707f16ce7d977748677cc354e250943b4ff6c21b9a19a492053d32397cf9ec",
|
"ondevice_hash": "59db25651da977eeb16a1af741fd01fc3d6b50d21544b1a7428b7c86b2cdef2d",
|
||||||
"alt": {
|
"alt": {
|
||||||
"hash": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39",
|
"hash": "c51bb5841011728f7cf108a9138ba68228ffb4232dfd91d6e082a6d8a6a8deaa",
|
||||||
"url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img",
|
"url": "https://commadist.azureedge.net/agnosupdate/system-c51bb5841011728f7cf108a9138ba68228ffb4232dfd91d6e082a6d8a6a8deaa.img",
|
||||||
"size": 5368709120
|
"size": 5368709120
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "userdata_90",
|
"name": "userdata_90",
|
||||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_90-f0c675e0fae420870c9ba8979fa246b170f4f1a7a04b49609b55b6bdfa8c1b21.img.xz",
|
"url": "https://commadist.azureedge.net/agnosupdate/userdata_90-89a161f17b86637413fe10a641550110b626b699382f5138c02267b7866a8494.img.xz",
|
||||||
"hash": "3d8a007bae088c5959eb9b82454013f91868946d78380fecea2b1afdfb575c02",
|
"hash": "99d9e6cf6755581c6879bbf442bd62212beb8a04116e965ab987135b8842188b",
|
||||||
"hash_raw": "f0c675e0fae420870c9ba8979fa246b170f4f1a7a04b49609b55b6bdfa8c1b21",
|
"hash_raw": "89a161f17b86637413fe10a641550110b626b699382f5138c02267b7866a8494",
|
||||||
"size": 96636764160,
|
"size": 96636764160,
|
||||||
"sparse": true,
|
"sparse": true,
|
||||||
"full_check": true,
|
"full_check": true,
|
||||||
"has_ab": false,
|
"has_ab": false,
|
||||||
"ondevice_hash": "5bfbabb8ff96b149056aa75d5b7e66a7cdd9cb4bcefe23b922c292f7f3a43462"
|
"ondevice_hash": "24ea29ab9c4ecec0568a4aa83e38790fedfce694060e90f4bde725931386ff41"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "userdata_89",
|
"name": "userdata_89",
|
||||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_89-06fc52be37b42690ed7b4f8c66c4611309a2dea9fca37dd9d27d1eff302eb1bf.img.xz",
|
"url": "https://commadist.azureedge.net/agnosupdate/userdata_89-cdd3401168819987c4840765bba1aa2217641b1a6a4165c412f44cac14ccfcbf.img.xz",
|
||||||
"hash": "443f136484294b210318842d09fb618d5411c8bdbab9f7421d8c89eb291a8d3f",
|
"hash": "5fbfa008a7f6b58ab01d4d171f3185924d4c9db69b54f4bfc0f214c6f17c2435",
|
||||||
"hash_raw": "06fc52be37b42690ed7b4f8c66c4611309a2dea9fca37dd9d27d1eff302eb1bf",
|
"hash_raw": "cdd3401168819987c4840765bba1aa2217641b1a6a4165c412f44cac14ccfcbf",
|
||||||
"size": 95563022336,
|
"size": 95563022336,
|
||||||
"sparse": true,
|
"sparse": true,
|
||||||
"full_check": true,
|
"full_check": true,
|
||||||
"has_ab": false,
|
"has_ab": false,
|
||||||
"ondevice_hash": "67db02b29a7e4435951c64cc962a474d048ed444aa912f3494391417cd51a074"
|
"ondevice_hash": "c07dc2e883a23d4a24d976cdf53a767a2fd699c8eeb476d60cdf18e84b417a52"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "userdata_30",
|
"name": "userdata_30",
|
||||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_30-06679488f0c5c3fcfd5f351133050751cd189f705e478a979c45fc4a166d18a6.img.xz",
|
"url": "https://commadist.azureedge.net/agnosupdate/userdata_30-2a8e8278b3bb545e6d7292c2417ccebdca9b47507eb5924f7c1e068737a7edfd.img.xz",
|
||||||
"hash": "875b580cb786f290a842e9187fd945657561886123eb3075a26f7995a18068f6",
|
"hash": "b3bc293c9c5e0480ef663e980c8ccb2fb83ffd230c85f8797830fb61b8f59360",
|
||||||
"hash_raw": "06679488f0c5c3fcfd5f351133050751cd189f705e478a979c45fc4a166d18a6",
|
"hash_raw": "2a8e8278b3bb545e6d7292c2417ccebdca9b47507eb5924f7c1e068737a7edfd",
|
||||||
"size": 32212254720,
|
"size": 32212254720,
|
||||||
"sparse": true,
|
"sparse": true,
|
||||||
"full_check": true,
|
"full_check": true,
|
||||||
"has_ab": false,
|
"has_ab": false,
|
||||||
"ondevice_hash": "16e27ba3c5cf9f0394ce6235ba6021b8a2de293fdb08399f8ca832fa5e4d0b9d"
|
"ondevice_hash": "8dae1cda089828c750d1d646337774ccd9432f567ecefde19a06dc7feeda9cd3"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
[connection]
|
|
||||||
id=esim
|
|
||||||
uuid=fff6553c-3284-4707-a6b1-acc021caaafb
|
|
||||||
type=gsm
|
|
||||||
permissions=
|
|
||||||
autoconnect=true
|
|
||||||
autoconnect-retries=100
|
|
||||||
autoconnect-priority=2
|
|
||||||
metered=1
|
|
||||||
|
|
||||||
[gsm]
|
|
||||||
apn=
|
|
||||||
home-only=false
|
|
||||||
auto-config=true
|
|
||||||
sim-id=
|
|
||||||
|
|
||||||
[ipv4]
|
|
||||||
route-metric=1000
|
|
||||||
dns-priority=1000
|
|
||||||
dns-search=
|
|
||||||
method=auto
|
|
||||||
|
|
||||||
[ipv6]
|
|
||||||
ddr-gen-mode=stable-privacy
|
|
||||||
dns-search=
|
|
||||||
route-metric=1000
|
|
||||||
dns-priority=1000
|
|
||||||
method=auto
|
|
||||||
|
|
||||||
[proxy]
|
|
||||||
@@ -3,7 +3,6 @@ import math
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
import tempfile
|
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from functools import cached_property, lru_cache
|
from functools import cached_property, lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -500,18 +499,19 @@ class Tici(HardwareBase):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# eSIM prime
|
# we use the lte connection built into AGNOS. cleanup esim connection if it exists
|
||||||
dest = "/etc/NetworkManager/system-connections/esim.nmconnection"
|
dest = "/etc/NetworkManager/system-connections/esim.nmconnection"
|
||||||
if sim_id.startswith('8985235') and not os.path.exists(dest):
|
if os.path.exists(dest):
|
||||||
with open(Path(__file__).parent/'esim.nmconnection') as f, tempfile.NamedTemporaryFile(mode='w') as tf:
|
os.system(f"sudo nmcli con delete {dest}")
|
||||||
dat = f.read()
|
self.reboot_modem()
|
||||||
dat = dat.replace("sim-id=", f"sim-id={sim_id}")
|
|
||||||
tf.write(dat)
|
|
||||||
tf.flush()
|
|
||||||
|
|
||||||
# needs to be root
|
def reboot_modem(self):
|
||||||
os.system(f"sudo cp {tf.name} {dest}")
|
modem = self.get_modem()
|
||||||
os.system(f"sudo nmcli con load {dest}")
|
for state in (0, 1):
|
||||||
|
try:
|
||||||
|
modem.Command(f'AT+CFUN={state}', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def get_networks(self):
|
def get_networks(self):
|
||||||
r = {}
|
r = {}
|
||||||
|
|||||||
@@ -5,16 +5,16 @@ from pathlib import Path
|
|||||||
|
|
||||||
# NOTE: Do NOT import anything here that needs be built (e.g. params)
|
# NOTE: Do NOT import anything here that needs be built (e.g. params)
|
||||||
from openpilot.common.basedir import BASEDIR
|
from openpilot.common.basedir import BASEDIR
|
||||||
from openpilot.common.spinner import Spinner
|
|
||||||
from openpilot.common.text_window import TextWindow
|
|
||||||
from openpilot.common.swaglog import cloudlog, add_file_handler
|
from openpilot.common.swaglog import cloudlog, add_file_handler
|
||||||
from openpilot.system.hardware import HARDWARE, AGNOS
|
from openpilot.system.hardware import HARDWARE, AGNOS
|
||||||
|
from openpilot.system.ui.spinner import Spinner
|
||||||
|
from openpilot.system.ui.text import TextWindow
|
||||||
from openpilot.system.version import get_build_metadata
|
from openpilot.system.version import get_build_metadata
|
||||||
|
|
||||||
MAX_CACHE_SIZE = 4e9 if "CI" in os.environ else 2e9
|
MAX_CACHE_SIZE = 4e9 if "CI" in os.environ else 2e9
|
||||||
CACHE_DIR = Path("/data/scons_cache" if AGNOS else "/tmp/scons_cache")
|
CACHE_DIR = Path("/data/scons_cache" if AGNOS else "/tmp/scons_cache")
|
||||||
|
|
||||||
TOTAL_SCONS_NODES = 3800
|
TOTAL_SCONS_NODES = 3765
|
||||||
MAX_BUILD_PROGRESS = 100
|
MAX_BUILD_PROGRESS = 100
|
||||||
|
|
||||||
def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None:
|
def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None:
|
||||||
@@ -88,7 +88,7 @@ def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
spinner = Spinner()
|
with Spinner() as spinner:
|
||||||
spinner.update_progress(0, 100)
|
spinner.update_progress(0, 100)
|
||||||
build_metadata = get_build_metadata()
|
build_metadata = get_build_metadata()
|
||||||
build(spinner, build_metadata.openpilot.is_dirty, minimal = AGNOS)
|
build(spinner, build_metadata.openpilot.is_dirty, minimal = AGNOS)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from cereal import log
|
|||||||
import cereal.messaging as messaging
|
import cereal.messaging as messaging
|
||||||
import openpilot.system.sentry as sentry
|
import openpilot.system.sentry as sentry
|
||||||
from openpilot.common.params import Params, ParamKeyType
|
from openpilot.common.params import Params, ParamKeyType
|
||||||
from openpilot.common.text_window import TextWindow
|
|
||||||
from openpilot.system.hardware import HARDWARE
|
from openpilot.system.hardware import HARDWARE
|
||||||
from openpilot.system.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog
|
from openpilot.system.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog
|
||||||
from openpilot.system.manager.process import ensure_running
|
from openpilot.system.manager.process import ensure_running
|
||||||
@@ -235,6 +234,8 @@ def main() -> None:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
from openpilot.system.ui.text import TextWindow
|
||||||
|
|
||||||
unblock_stdout()
|
unblock_stdout()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ procs = [
|
|||||||
|
|
||||||
PythonProcess("sensord", "system.sensord.sensord", only_onroad, enabled=not PC),
|
PythonProcess("sensord", "system.sensord.sensord", only_onroad, enabled=not PC),
|
||||||
NativeProcess("ui", "selfdrive/ui", ["./ui"], always_run, watchdog_max_dt=(5 if not PC else None)),
|
NativeProcess("ui", "selfdrive/ui", ["./ui"], always_run, watchdog_max_dt=(5 if not PC else None)),
|
||||||
PythonProcess("soundd", "selfdrive.ui.soundd", only_onroad),
|
PythonProcess("soundd", "selfdrive.ui.soundd", and_(only_onroad, not_joystick)),
|
||||||
PythonProcess("locationd", "selfdrive.locationd.locationd", only_onroad),
|
PythonProcess("locationd", "selfdrive.locationd.locationd", only_onroad),
|
||||||
NativeProcess("_pandad", "selfdrive/pandad", ["./pandad"], always_run, enabled=False),
|
NativeProcess("_pandad", "selfdrive/pandad", ["./pandad"], always_run, enabled=False),
|
||||||
PythonProcess("calibrationd", "selfdrive.locationd.calibrationd", only_onroad),
|
PythonProcess("calibrationd", "selfdrive.locationd.calibrationd", only_onroad),
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import atexit
|
import atexit
|
||||||
import cffi
|
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
from collections.abc import Callable
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from importlib.resources import as_file, files
|
from importlib.resources import as_file, files
|
||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
@@ -15,7 +12,7 @@ FPS_LOG_INTERVAL = 5 # Seconds between logging FPS drops
|
|||||||
FPS_DROP_THRESHOLD = 0.9 # FPS drop threshold for triggering a warning
|
FPS_DROP_THRESHOLD = 0.9 # FPS drop threshold for triggering a warning
|
||||||
FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions
|
FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions
|
||||||
|
|
||||||
ENABLE_VSYNC = os.getenv("ENABLE_VSYNC", "1") == "1"
|
ENABLE_VSYNC = os.getenv("ENABLE_VSYNC") == "1"
|
||||||
SHOW_FPS = os.getenv("SHOW_FPS") == '1'
|
SHOW_FPS = os.getenv("SHOW_FPS") == '1'
|
||||||
STRICT_MODE = os.getenv("STRICT_MODE") == '1'
|
STRICT_MODE = os.getenv("STRICT_MODE") == '1'
|
||||||
SCALE = float(os.getenv("SCALE", "1.0"))
|
SCALE = float(os.getenv("SCALE", "1.0"))
|
||||||
@@ -39,12 +36,6 @@ class FontWeight(IntEnum):
|
|||||||
BLACK = 8
|
BLACK = 8
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ModalOverlay:
|
|
||||||
overlay: object = None
|
|
||||||
callback: Callable | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class GuiApplication:
|
class GuiApplication:
|
||||||
def __init__(self, width: int, height: int):
|
def __init__(self, width: int, height: int):
|
||||||
self._fonts: dict[FontWeight, rl.Font] = {}
|
self._fonts: dict[FontWeight, rl.Font] = {}
|
||||||
@@ -59,7 +50,6 @@ class GuiApplication:
|
|||||||
self._last_fps_log_time: float = time.monotonic()
|
self._last_fps_log_time: float = time.monotonic()
|
||||||
self._window_close_requested = False
|
self._window_close_requested = False
|
||||||
self._trace_log_callback = None
|
self._trace_log_callback = None
|
||||||
self._modal_overlay = ModalOverlay()
|
|
||||||
|
|
||||||
def request_close(self):
|
def request_close(self):
|
||||||
self._window_close_requested = True
|
self._window_close_requested = True
|
||||||
@@ -89,9 +79,6 @@ class GuiApplication:
|
|||||||
self._set_styles()
|
self._set_styles()
|
||||||
self._load_fonts()
|
self._load_fonts()
|
||||||
|
|
||||||
def set_modal_overlay(self, overlay, callback: Callable | None = None):
|
|
||||||
self._modal_overlay = ModalOverlay(overlay=overlay, callback=callback)
|
|
||||||
|
|
||||||
def texture(self, asset_path: str, width: int, height: int, alpha_premultiply=False, keep_aspect_ratio=True):
|
def texture(self, asset_path: str, width: int, height: int, alpha_premultiply=False, keep_aspect_ratio=True):
|
||||||
cache_key = f"{asset_path}_{width}_{height}_{alpha_premultiply}{keep_aspect_ratio}"
|
cache_key = f"{asset_path}_{width}_{height}_{alpha_premultiply}{keep_aspect_ratio}"
|
||||||
if cache_key in self._textures:
|
if cache_key in self._textures:
|
||||||
@@ -161,23 +148,7 @@ class GuiApplication:
|
|||||||
rl.begin_drawing()
|
rl.begin_drawing()
|
||||||
rl.clear_background(rl.BLACK)
|
rl.clear_background(rl.BLACK)
|
||||||
|
|
||||||
# Handle modal overlay rendering and input processing
|
yield
|
||||||
if self._modal_overlay.overlay:
|
|
||||||
if hasattr(self._modal_overlay.overlay, 'render'):
|
|
||||||
result = self._modal_overlay.overlay.render(rl.Rectangle(0, 0, self.width, self.height))
|
|
||||||
elif callable(self._modal_overlay.overlay):
|
|
||||||
result = self._modal_overlay.overlay()
|
|
||||||
else:
|
|
||||||
raise Exception
|
|
||||||
|
|
||||||
if result >= 0:
|
|
||||||
# Execute callback with the result and clear the overlay
|
|
||||||
if self._modal_overlay.callback is not None:
|
|
||||||
self._modal_overlay.callback(result)
|
|
||||||
|
|
||||||
self._modal_overlay = ModalOverlay()
|
|
||||||
else:
|
|
||||||
yield
|
|
||||||
|
|
||||||
if self._render_texture:
|
if self._render_texture:
|
||||||
rl.end_texture_mode()
|
rl.end_texture_mode()
|
||||||
@@ -221,11 +192,12 @@ class GuiApplication:
|
|||||||
|
|
||||||
# Create a character set from our keyboard layouts
|
# Create a character set from our keyboard layouts
|
||||||
from openpilot.system.ui.widgets.keyboard import KEYBOARD_LAYOUTS
|
from openpilot.system.ui.widgets.keyboard import KEYBOARD_LAYOUTS
|
||||||
|
from openpilot.selfdrive.ui.onroad.hud_renderer import CRUISE_DISABLED_CHAR
|
||||||
all_chars = set()
|
all_chars = set()
|
||||||
for layout in KEYBOARD_LAYOUTS.values():
|
for layout in KEYBOARD_LAYOUTS.values():
|
||||||
all_chars.update(key for row in layout for key in row)
|
all_chars.update(key for row in layout for key in row)
|
||||||
all_chars = "".join(all_chars)
|
all_chars = "".join(all_chars)
|
||||||
all_chars += "–✓°"
|
all_chars += CRUISE_DISABLED_CHAR
|
||||||
|
|
||||||
codepoint_count = rl.ffi.new("int *", 1)
|
codepoint_count = rl.ffi.new("int *", 1)
|
||||||
codepoints = rl.load_codepoints(all_chars, codepoint_count)
|
codepoints = rl.load_codepoints(all_chars, codepoint_count)
|
||||||
@@ -247,29 +219,12 @@ class GuiApplication:
|
|||||||
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BASE_COLOR_NORMAL, rl.color_to_int(rl.Color(50, 50, 50, 255)))
|
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BASE_COLOR_NORMAL, rl.color_to_int(rl.Color(50, 50, 50, 255)))
|
||||||
|
|
||||||
def _set_log_callback(self):
|
def _set_log_callback(self):
|
||||||
ffi_libc = cffi.FFI()
|
|
||||||
ffi_libc.cdef("""
|
|
||||||
int vasprintf(char **strp, const char *fmt, void *ap);
|
|
||||||
void free(void *ptr);
|
|
||||||
""")
|
|
||||||
libc = ffi_libc.dlopen(None)
|
|
||||||
|
|
||||||
@rl.ffi.callback("void(int, char *, void *)")
|
@rl.ffi.callback("void(int, char *, void *)")
|
||||||
def trace_log_callback(log_level, text, args):
|
def trace_log_callback(log_level, text, args):
|
||||||
try:
|
try:
|
||||||
text_addr = int(rl.ffi.cast("uintptr_t", text))
|
text_str = rl.ffi.string(text).decode('utf-8')
|
||||||
args_addr = int(rl.ffi.cast("uintptr_t", args))
|
except (TypeError, UnicodeDecodeError):
|
||||||
text_libc = ffi_libc.cast("char *", text_addr)
|
text_str = str(text)
|
||||||
args_libc = ffi_libc.cast("void *", args_addr)
|
|
||||||
|
|
||||||
out = ffi_libc.new("char **")
|
|
||||||
if libc.vasprintf(out, text_libc, args_libc) >= 0 and out[0] != ffi_libc.NULL:
|
|
||||||
text_str = ffi_libc.string(out[0]).decode("utf-8", "replace")
|
|
||||||
libc.free(out[0])
|
|
||||||
else:
|
|
||||||
text_str = rl.ffi.string(text).decode("utf-8", "replace")
|
|
||||||
except Exception as e:
|
|
||||||
text_str = f"[Log decode error: {e}]"
|
|
||||||
|
|
||||||
if log_level == rl.TraceLogLevel.LOG_ERROR:
|
if log_level == rl.TraceLogLevel.LOG_ERROR:
|
||||||
cloudlog.error(f"raylib: {text_str}")
|
cloudlog.error(f"raylib: {text_str}")
|
||||||
|
|||||||
+4
-13
@@ -10,7 +10,6 @@ class ButtonStyle(IntEnum):
|
|||||||
DANGER = 2 # For critical actions, like reboot or delete
|
DANGER = 2 # For critical actions, like reboot or delete
|
||||||
TRANSPARENT = 3 # For buttons with transparent background and border
|
TRANSPARENT = 3 # For buttons with transparent background and border
|
||||||
ACTION = 4
|
ACTION = 4
|
||||||
LIST_ACTION = 5 # For list items with action buttons
|
|
||||||
|
|
||||||
|
|
||||||
class TextAlignment(IntEnum):
|
class TextAlignment(IntEnum):
|
||||||
@@ -21,17 +20,11 @@ class TextAlignment(IntEnum):
|
|||||||
|
|
||||||
ICON_PADDING = 15
|
ICON_PADDING = 15
|
||||||
DEFAULT_BUTTON_FONT_SIZE = 60
|
DEFAULT_BUTTON_FONT_SIZE = 60
|
||||||
|
BUTTON_ENABLED_TEXT_COLOR = rl.Color(228, 228, 228, 255)
|
||||||
BUTTON_DISABLED_TEXT_COLOR = rl.Color(228, 228, 228, 51)
|
BUTTON_DISABLED_TEXT_COLOR = rl.Color(228, 228, 228, 51)
|
||||||
ACTION_BUTTON_FONT_SIZE = 48
|
ACTION_BUTTON_FONT_SIZE = 48
|
||||||
|
ACTION_BUTTON_TEXT_COLOR = rl.Color(0, 0, 0, 255)
|
||||||
|
|
||||||
BUTTON_TEXT_COLOR = {
|
|
||||||
ButtonStyle.NORMAL: rl.Color(228, 228, 228, 255),
|
|
||||||
ButtonStyle.PRIMARY: rl.Color(228, 228, 228, 255),
|
|
||||||
ButtonStyle.DANGER: rl.Color(228, 228, 228, 255),
|
|
||||||
ButtonStyle.TRANSPARENT: rl.BLACK,
|
|
||||||
ButtonStyle.ACTION: rl.Color(0, 0, 0, 255),
|
|
||||||
ButtonStyle.LIST_ACTION: rl.Color(228, 228, 228, 255),
|
|
||||||
}
|
|
||||||
|
|
||||||
BUTTON_BACKGROUND_COLORS = {
|
BUTTON_BACKGROUND_COLORS = {
|
||||||
ButtonStyle.NORMAL: rl.Color(51, 51, 51, 255),
|
ButtonStyle.NORMAL: rl.Color(51, 51, 51, 255),
|
||||||
@@ -39,7 +32,6 @@ BUTTON_BACKGROUND_COLORS = {
|
|||||||
ButtonStyle.DANGER: rl.Color(255, 36, 36, 255),
|
ButtonStyle.DANGER: rl.Color(255, 36, 36, 255),
|
||||||
ButtonStyle.TRANSPARENT: rl.BLACK,
|
ButtonStyle.TRANSPARENT: rl.BLACK,
|
||||||
ButtonStyle.ACTION: rl.Color(189, 189, 189, 255),
|
ButtonStyle.ACTION: rl.Color(189, 189, 189, 255),
|
||||||
ButtonStyle.LIST_ACTION: rl.Color(57, 57, 57, 255),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BUTTON_PRESSED_BACKGROUND_COLORS = {
|
BUTTON_PRESSED_BACKGROUND_COLORS = {
|
||||||
@@ -48,7 +40,6 @@ BUTTON_PRESSED_BACKGROUND_COLORS = {
|
|||||||
ButtonStyle.DANGER: rl.Color(255, 36, 36, 255),
|
ButtonStyle.DANGER: rl.Color(255, 36, 36, 255),
|
||||||
ButtonStyle.TRANSPARENT: rl.BLACK,
|
ButtonStyle.TRANSPARENT: rl.BLACK,
|
||||||
ButtonStyle.ACTION: rl.Color(130, 130, 130, 255),
|
ButtonStyle.ACTION: rl.Color(130, 130, 130, 255),
|
||||||
ButtonStyle.LIST_ACTION: rl.Color(74, 74, 74, 74),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_pressed_buttons: set[str] = set() # Track mouse press state globally
|
_pressed_buttons: set[str] = set() # Track mouse press state globally
|
||||||
@@ -142,7 +133,7 @@ def gui_button(
|
|||||||
|
|
||||||
# Draw the button text if any
|
# Draw the button text if any
|
||||||
if text:
|
if text:
|
||||||
color = BUTTON_TEXT_COLOR[button_style] if is_enabled else BUTTON_DISABLED_TEXT_COLOR
|
text_color = ACTION_BUTTON_TEXT_COLOR if button_style == ButtonStyle.ACTION else BUTTON_ENABLED_TEXT_COLOR if is_enabled else BUTTON_DISABLED_TEXT_COLOR
|
||||||
rl.draw_text_ex(font, text, text_pos, font_size, 0, color)
|
rl.draw_text_ex(font, text, text_pos, font_size, 0, text_color)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from dataclasses import dataclass
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
|
|
||||||
|
|
||||||
# EGL constants
|
# EGL constants
|
||||||
EGL_LINUX_DMA_BUF_EXT = 0x3270
|
EGL_LINUX_DMA_BUF_EXT = 0x3270
|
||||||
EGL_WIDTH = 0x3057
|
EGL_WIDTH = 0x3057
|
||||||
@@ -22,7 +23,6 @@ GL_TEXTURE_EXTERNAL_OES = 0x8D65
|
|||||||
# DRM Format for NV12
|
# DRM Format for NV12
|
||||||
DRM_FORMAT_NV12 = 842094158
|
DRM_FORMAT_NV12 = 842094158
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EGLImage:
|
class EGLImage:
|
||||||
"""Container for EGL image and associated resources"""
|
"""Container for EGL image and associated resources"""
|
||||||
|
|||||||
+11
-12
@@ -2,15 +2,14 @@ import pyray as rl
|
|||||||
import time
|
import time
|
||||||
from openpilot.system.ui.lib.application import gui_app
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.lib.widget import Widget
|
|
||||||
|
|
||||||
PASSWORD_MASK_CHAR = "•"
|
PASSWORD_MASK_CHAR = "•"
|
||||||
PASSWORD_MASK_DELAY = 1.5 # Seconds to show character before masking
|
PASSWORD_MASK_DELAY = 1.5 # Seconds to show character before masking
|
||||||
|
|
||||||
|
|
||||||
class InputBox(Widget):
|
class InputBox:
|
||||||
def __init__(self, max_text_size=255, password_mode=False):
|
def __init__(self, max_text_size=255, password_mode=False):
|
||||||
super().__init__()
|
|
||||||
self._max_text_size = max_text_size
|
self._max_text_size = max_text_size
|
||||||
self._input_text = ""
|
self._input_text = ""
|
||||||
self._cursor_position = 0
|
self._cursor_position = 0
|
||||||
@@ -24,7 +23,7 @@ class InputBox(Widget):
|
|||||||
self._text_offset = 0
|
self._text_offset = 0
|
||||||
self._visible_width = 0
|
self._visible_width = 0
|
||||||
self._last_char_time = 0 # Track when last character was added
|
self._last_char_time = 0 # Track when last character was added
|
||||||
self._masked_length = 0 # How many characters are currently masked
|
self._masked_length = 0 # How many characters are currently masked
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def text(self):
|
def text(self):
|
||||||
@@ -77,11 +76,11 @@ class InputBox(Widget):
|
|||||||
def add_char_at_cursor(self, char):
|
def add_char_at_cursor(self, char):
|
||||||
"""Add a character at the current cursor position."""
|
"""Add a character at the current cursor position."""
|
||||||
if len(self._input_text) < self._max_text_size:
|
if len(self._input_text) < self._max_text_size:
|
||||||
self._input_text = self._input_text[: self._cursor_position] + char + self._input_text[self._cursor_position:]
|
self._input_text = self._input_text[: self._cursor_position] + char + self._input_text[self._cursor_position :]
|
||||||
self.set_cursor_position(self._cursor_position + 1)
|
self.set_cursor_position(self._cursor_position + 1)
|
||||||
|
|
||||||
if self._password_mode:
|
if self._password_mode:
|
||||||
self._last_char_time = time.monotonic()
|
self._last_char_time = time.time()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -89,7 +88,7 @@ class InputBox(Widget):
|
|||||||
def delete_char_before_cursor(self):
|
def delete_char_before_cursor(self):
|
||||||
"""Delete the character before the cursor position (backspace)."""
|
"""Delete the character before the cursor position (backspace)."""
|
||||||
if self._cursor_position > 0:
|
if self._cursor_position > 0:
|
||||||
self._input_text = self._input_text[: self._cursor_position - 1] + self._input_text[self._cursor_position:]
|
self._input_text = self._input_text[: self._cursor_position - 1] + self._input_text[self._cursor_position :]
|
||||||
self.set_cursor_position(self._cursor_position - 1)
|
self.set_cursor_position(self._cursor_position - 1)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -97,12 +96,12 @@ class InputBox(Widget):
|
|||||||
def delete_char_at_cursor(self):
|
def delete_char_at_cursor(self):
|
||||||
"""Delete the character at the cursor position (delete)."""
|
"""Delete the character at the cursor position (delete)."""
|
||||||
if self._cursor_position < len(self._input_text):
|
if self._cursor_position < len(self._input_text):
|
||||||
self._input_text = self._input_text[: self._cursor_position] + self._input_text[self._cursor_position + 1:]
|
self._input_text = self._input_text[: self._cursor_position] + self._input_text[self._cursor_position + 1 :]
|
||||||
self.set_cursor_position(self._cursor_position)
|
self.set_cursor_position(self._cursor_position)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _render(self, rect, color=rl.BLACK, border_color=rl.DARKGRAY, text_color=rl.WHITE, font_size=80):
|
def render(self, rect, color=rl.BLACK, border_color=rl.DARKGRAY, text_color=rl.WHITE, font_size=80):
|
||||||
# Store dimensions for text offset calculations
|
# Store dimensions for text offset calculations
|
||||||
self._visible_width = rect.width
|
self._visible_width = rect.width
|
||||||
self._font_size = font_size
|
self._font_size = font_size
|
||||||
@@ -161,18 +160,18 @@ class InputBox(Widget):
|
|||||||
|
|
||||||
# Show character at last edited position if within delay window
|
# Show character at last edited position if within delay window
|
||||||
masked_text = PASSWORD_MASK_CHAR * len(self._input_text)
|
masked_text = PASSWORD_MASK_CHAR * len(self._input_text)
|
||||||
recent_edit = time.monotonic() - self._last_char_time < PASSWORD_MASK_DELAY
|
recent_edit = time.time() - self._last_char_time < PASSWORD_MASK_DELAY
|
||||||
if recent_edit and self._input_text:
|
if recent_edit and self._input_text:
|
||||||
last_pos = max(0, self._cursor_position - 1)
|
last_pos = max(0, self._cursor_position - 1)
|
||||||
if last_pos < len(self._input_text):
|
if last_pos < len(self._input_text):
|
||||||
return masked_text[:last_pos] + self._input_text[last_pos] + masked_text[last_pos + 1:]
|
return masked_text[:last_pos] + self._input_text[last_pos] + masked_text[last_pos + 1 :]
|
||||||
|
|
||||||
return masked_text
|
return masked_text
|
||||||
|
|
||||||
def _handle_mouse_input(self, rect, font_size):
|
def _handle_mouse_input(self, rect, font_size):
|
||||||
"""Handle mouse clicks to position cursor."""
|
"""Handle mouse clicks to position cursor."""
|
||||||
mouse_pos = rl.get_mouse_position()
|
mouse_pos = rl.get_mouse_position()
|
||||||
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT) and rl.check_collision_point_rec(mouse_pos, rect):
|
if rl.is_mouse_button_pressed(rl.MOUSE_LEFT_BUTTON) and rl.check_collision_point_rec(mouse_pos, rect):
|
||||||
# Calculate cursor position from click
|
# Calculate cursor position from click
|
||||||
if len(self._input_text) > 0:
|
if len(self._input_text) > 0:
|
||||||
font = gui_app.font()
|
font = gui_app.font()
|
||||||
|
|||||||
@@ -76,3 +76,4 @@ def gui_text_box(
|
|||||||
|
|
||||||
if font_weight != FontWeight.NORMAL:
|
if font_weight != FontWeight.NORMAL:
|
||||||
rl.gui_set_font(gui_app.font(FontWeight.NORMAL))
|
rl.gui_set_font(gui_app.font(FontWeight.NORMAL))
|
||||||
|
|
||||||
|
|||||||
+281
-269
@@ -1,22 +1,27 @@
|
|||||||
import os
|
import os
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
from dataclasses import dataclass
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from abc import ABC
|
from abc import ABC, abstractmethod
|
||||||
|
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
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.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||||
from openpilot.system.ui.lib.button import gui_button, ButtonStyle
|
from openpilot.system.ui.lib.button import gui_button
|
||||||
from openpilot.system.ui.lib.toggle import Toggle, WIDTH as TOGGLE_WIDTH, HEIGHT as TOGGLE_HEIGHT
|
from openpilot.system.ui.lib.toggle import Toggle
|
||||||
from openpilot.system.ui.lib.widget import Widget
|
from openpilot.system.ui.lib.toggle import WIDTH as TOGGLE_WIDTH, HEIGHT as TOGGLE_HEIGHT
|
||||||
|
|
||||||
ITEM_BASE_WIDTH = 600
|
|
||||||
ITEM_BASE_HEIGHT = 170
|
LINE_PADDING = 40
|
||||||
|
LINE_COLOR = rl.GRAY
|
||||||
ITEM_PADDING = 20
|
ITEM_PADDING = 20
|
||||||
|
ITEM_SPACING = 80
|
||||||
|
ITEM_BASE_HEIGHT = 170
|
||||||
ITEM_TEXT_FONT_SIZE = 50
|
ITEM_TEXT_FONT_SIZE = 50
|
||||||
ITEM_TEXT_COLOR = rl.WHITE
|
ITEM_TEXT_COLOR = rl.WHITE
|
||||||
ITEM_DESC_TEXT_COLOR = rl.Color(128, 128, 128, 255)
|
ITEM_DESC_TEXT_COLOR = rl.Color(128, 128, 128, 255)
|
||||||
ITEM_DESC_FONT_SIZE = 40
|
ITEM_DESC_FONT_SIZE = 40
|
||||||
ITEM_DESC_V_OFFSET = 140
|
ITEM_DESC_V_OFFSET = 130
|
||||||
RIGHT_ITEM_PADDING = 20
|
RIGHT_ITEM_PADDING = 20
|
||||||
ICON_SIZE = 80
|
ICON_SIZE = 80
|
||||||
BUTTON_WIDTH = 250
|
BUTTON_WIDTH = 250
|
||||||
@@ -25,42 +30,38 @@ BUTTON_BORDER_RADIUS = 50
|
|||||||
BUTTON_FONT_SIZE = 35
|
BUTTON_FONT_SIZE = 35
|
||||||
BUTTON_FONT_WEIGHT = FontWeight.MEDIUM
|
BUTTON_FONT_WEIGHT = FontWeight.MEDIUM
|
||||||
|
|
||||||
TEXT_PADDING = 20
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_value(value, default=""):
|
|
||||||
if callable(value):
|
|
||||||
return value()
|
|
||||||
return value if value is not None else default
|
|
||||||
|
|
||||||
|
|
||||||
# Abstract base class for right-side items
|
# Abstract base class for right-side items
|
||||||
class ItemAction(Widget, ABC):
|
class RightItem(ABC):
|
||||||
def __init__(self, width: int = BUTTON_HEIGHT, enabled: bool | Callable[[], bool] = True):
|
def __init__(self, width: int = 100):
|
||||||
super().__init__()
|
self.width = width
|
||||||
self.set_rect(rl.Rectangle(0, 0, width, 0))
|
self.enabled = True
|
||||||
self._enabled_source = enabled
|
|
||||||
|
|
||||||
@property
|
@abstractmethod
|
||||||
def enabled(self):
|
def draw(self, rect: rl.Rectangle) -> bool:
|
||||||
return _resolve_value(self._enabled_source, False)
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_width(self) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ToggleAction(ItemAction):
|
class ToggleRightItem(RightItem):
|
||||||
def __init__(self, initial_state: bool = False, width: int = TOGGLE_WIDTH, enabled: bool | Callable[[], bool] = True):
|
def __init__(self, initial_state: bool = False, width: int = TOGGLE_WIDTH):
|
||||||
super().__init__(width, enabled)
|
super().__init__(width)
|
||||||
self.toggle = Toggle(initial_state=initial_state)
|
self.toggle = Toggle(initial_state=initial_state)
|
||||||
self.state = initial_state
|
self.state = initial_state
|
||||||
|
self.enabled = True
|
||||||
|
|
||||||
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
|
def draw(self, rect: rl.Rectangle) -> bool:
|
||||||
super().set_touch_valid_callback(touch_callback)
|
if self.toggle.render(rl.Rectangle(rect.x, rect.y + (rect.height - TOGGLE_HEIGHT) / 2, self.width, TOGGLE_HEIGHT)):
|
||||||
self.toggle.set_touch_valid_callback(touch_callback)
|
self.state = not self.state
|
||||||
|
return True
|
||||||
def _render(self, rect: rl.Rectangle) -> bool:
|
|
||||||
self.toggle.set_enabled(self.enabled)
|
|
||||||
self.toggle.render(rl.Rectangle(rect.x, rect.y + (rect.height - TOGGLE_HEIGHT) / 2, self._rect.width, TOGGLE_HEIGHT))
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_width(self) -> int:
|
||||||
|
return self.width
|
||||||
|
|
||||||
def set_state(self, state: bool):
|
def set_state(self, state: bool):
|
||||||
self.state = state
|
self.state = state
|
||||||
self.toggle.set_state(state)
|
self.toggle.set_state(state)
|
||||||
@@ -68,269 +69,293 @@ class ToggleAction(ItemAction):
|
|||||||
def get_state(self) -> bool:
|
def get_state(self) -> bool:
|
||||||
return self.state
|
return self.state
|
||||||
|
|
||||||
|
def set_enabled(self, enabled: bool):
|
||||||
class ButtonAction(ItemAction):
|
self.enabled = enabled
|
||||||
def __init__(self, text: str | Callable[[], str], width: int = BUTTON_WIDTH, enabled: bool | Callable[[], bool] = True):
|
|
||||||
super().__init__(width, enabled)
|
|
||||||
self._text_source = text
|
|
||||||
|
|
||||||
@property
|
|
||||||
def text(self):
|
|
||||||
return _resolve_value(self._text_source, "Error")
|
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle) -> bool:
|
|
||||||
return gui_button(
|
|
||||||
rl.Rectangle(rect.x, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT),
|
|
||||||
self.text,
|
|
||||||
border_radius=BUTTON_BORDER_RADIUS,
|
|
||||||
font_weight=BUTTON_FONT_WEIGHT,
|
|
||||||
font_size=BUTTON_FONT_SIZE,
|
|
||||||
button_style=ButtonStyle.LIST_ACTION,
|
|
||||||
is_enabled=self.enabled,
|
|
||||||
) == 1
|
|
||||||
|
|
||||||
|
|
||||||
class TextAction(ItemAction):
|
class ButtonRightItem(RightItem):
|
||||||
def __init__(self, text: str | Callable[[], str], color: rl.Color = ITEM_TEXT_COLOR, enabled: bool | Callable[[], bool] = True):
|
def __init__(self, text: str, width: int = BUTTON_WIDTH):
|
||||||
self._text_source = text
|
super().__init__(width)
|
||||||
|
self.text = text
|
||||||
|
self.enabled = True
|
||||||
|
|
||||||
|
def draw(self, rect: rl.Rectangle) -> bool:
|
||||||
|
return (
|
||||||
|
gui_button(
|
||||||
|
rl.Rectangle(rect.x, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT),
|
||||||
|
self.text,
|
||||||
|
border_radius=BUTTON_BORDER_RADIUS,
|
||||||
|
font_weight=BUTTON_FONT_WEIGHT,
|
||||||
|
font_size=BUTTON_FONT_SIZE,
|
||||||
|
is_enabled=self.enabled,
|
||||||
|
)
|
||||||
|
== 1
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_width(self) -> int:
|
||||||
|
return self.width
|
||||||
|
|
||||||
|
def set_enabled(self, enabled: bool):
|
||||||
|
self.enabled = enabled
|
||||||
|
|
||||||
|
|
||||||
|
class TextRightItem(RightItem):
|
||||||
|
def __init__(self, text: str, color: rl.Color = ITEM_TEXT_COLOR, font_size: int = ITEM_TEXT_FONT_SIZE):
|
||||||
|
self.text = text
|
||||||
self.color = color
|
self.color = color
|
||||||
|
self.font_size = font_size
|
||||||
|
|
||||||
self._font = gui_app.font(FontWeight.NORMAL)
|
font = gui_app.font(FontWeight.NORMAL)
|
||||||
initial_text = _resolve_value(text, "")
|
text_width = measure_text_cached(font, text, font_size).x
|
||||||
text_width = measure_text_cached(self._font, initial_text, ITEM_TEXT_FONT_SIZE).x
|
super().__init__(int(text_width + 20))
|
||||||
super().__init__(int(text_width + TEXT_PADDING), enabled)
|
|
||||||
|
|
||||||
@property
|
def draw(self, rect: rl.Rectangle) -> bool:
|
||||||
def text(self):
|
font = gui_app.font(FontWeight.NORMAL)
|
||||||
return _resolve_value(self._text_source, "Error")
|
text_size = measure_text_cached(font, self.text, self.font_size)
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle) -> bool:
|
|
||||||
current_text = self.text
|
|
||||||
text_size = measure_text_cached(self._font, current_text, ITEM_TEXT_FONT_SIZE)
|
|
||||||
|
|
||||||
|
# Center the text in the allocated rectangle
|
||||||
text_x = rect.x + (rect.width - text_size.x) / 2
|
text_x = rect.x + (rect.width - text_size.x) / 2
|
||||||
text_y = rect.y + (rect.height - text_size.y) / 2
|
text_y = rect.y + (rect.height - text_size.y) / 2
|
||||||
rl.draw_text_ex(self._font, current_text, rl.Vector2(text_x, text_y), ITEM_TEXT_FONT_SIZE, 0, self.color)
|
|
||||||
|
rl.draw_text_ex(font, self.text, rl.Vector2(text_x, text_y), self.font_size, 0, self.color)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_width(self) -> int:
|
def get_width(self) -> int:
|
||||||
text_width = measure_text_cached(self._font, self.text, ITEM_TEXT_FONT_SIZE).x
|
return self.width
|
||||||
return int(text_width + TEXT_PADDING)
|
|
||||||
|
def set_text(self, text: str):
|
||||||
|
self.text = text
|
||||||
|
font = gui_app.font(FontWeight.NORMAL)
|
||||||
|
text_width = measure_text_cached(font, text, self.font_size).x
|
||||||
|
self.width = int(text_width + 20)
|
||||||
|
|
||||||
|
|
||||||
class DualButtonAction(ItemAction):
|
@dataclass
|
||||||
def __init__(self, left_text: str, right_text: str, left_callback: Callable = None,
|
class ListItem:
|
||||||
right_callback: Callable = None, enabled: bool | Callable[[], bool] = True):
|
title: str
|
||||||
super().__init__(width=0, enabled=enabled) # Width 0 means use full width
|
icon: str | None = None
|
||||||
self.left_text, self.right_text = left_text, right_text
|
description: str | None = None
|
||||||
self.left_callback, self.right_callback = left_callback, right_callback
|
description_visible: bool = False
|
||||||
|
rect: "rl.Rectangle | None" = None
|
||||||
|
callback: Callable | None = None
|
||||||
|
right_item: RightItem | None = None
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle) -> bool:
|
# Cached properties for performance
|
||||||
button_spacing = 30
|
_wrapped_description: str | None = None
|
||||||
button_height = 120
|
_description_height: float = 0
|
||||||
button_width = (rect.width - button_spacing) / 2
|
|
||||||
button_y = rect.y + (rect.height - button_height) / 2
|
|
||||||
|
|
||||||
left_rect = rl.Rectangle(rect.x, button_y, button_width, button_height)
|
def get_right_item(self) -> RightItem | None:
|
||||||
right_rect = rl.Rectangle(rect.x + button_width + button_spacing, button_y, button_width, button_height)
|
return self.right_item
|
||||||
|
|
||||||
left_clicked = gui_button(left_rect, self.left_text, button_style=ButtonStyle.LIST_ACTION) == 1
|
def get_item_height(self, font: rl.Font, max_width: int) -> float:
|
||||||
right_clicked = gui_button(right_rect, self.right_text, button_style=ButtonStyle.DANGER) == 1
|
if self.description_visible and self.description:
|
||||||
|
if not self._wrapped_description:
|
||||||
|
wrapped_lines = wrap_text(font, self.description, ITEM_DESC_FONT_SIZE, max_width)
|
||||||
|
self._wrapped_description = "\n".join(wrapped_lines)
|
||||||
|
self._description_height = len(wrapped_lines) * 20 + 10 # Line height + padding
|
||||||
|
return ITEM_BASE_HEIGHT + self._description_height - (ITEM_BASE_HEIGHT - ITEM_DESC_V_OFFSET) + ITEM_SPACING
|
||||||
|
return ITEM_BASE_HEIGHT
|
||||||
|
|
||||||
if left_clicked and self.left_callback:
|
def get_content_width(self, total_width: int) -> int:
|
||||||
self.left_callback()
|
if self.right_item:
|
||||||
return True
|
return total_width - self.right_item.get_width() - RIGHT_ITEM_PADDING
|
||||||
if right_clicked and self.right_callback:
|
return total_width
|
||||||
self.right_callback()
|
|
||||||
return True
|
def get_right_item_rect(self, item_rect: rl.Rectangle) -> rl.Rectangle:
|
||||||
return False
|
if not self.right_item:
|
||||||
|
return rl.Rectangle(0, 0, 0, 0)
|
||||||
|
|
||||||
|
right_width = self.right_item.get_width()
|
||||||
|
right_x = item_rect.x + item_rect.width - right_width
|
||||||
|
right_y = item_rect.y
|
||||||
|
return rl.Rectangle(right_x, right_y, right_width, ITEM_BASE_HEIGHT)
|
||||||
|
|
||||||
|
|
||||||
class MultipleButtonAction(ItemAction):
|
class ListView:
|
||||||
def __init__(self, buttons: list[str], button_width: int, selected_index: int = 0, callback: Callable = None):
|
def __init__(self, items: list[ListItem]):
|
||||||
super().__init__(width=len(buttons) * (button_width + 20), enabled=True)
|
self._items: list[ListItem] = items
|
||||||
self.buttons = buttons
|
self._last_dim: tuple[float, float] = (0, 0)
|
||||||
self.button_width = button_width
|
self.scroll_panel = GuiScrollPanel()
|
||||||
self.selected_button = selected_index
|
|
||||||
self.callback = callback
|
|
||||||
self._font = gui_app.font(FontWeight.MEDIUM)
|
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle) -> bool:
|
self._font_normal = gui_app.font(FontWeight.NORMAL)
|
||||||
spacing = 20
|
|
||||||
button_y = rect.y + (rect.height - BUTTON_HEIGHT) / 2
|
|
||||||
clicked = -1
|
|
||||||
|
|
||||||
for i, text in enumerate(self.buttons):
|
# Interaction state
|
||||||
button_x = rect.x + i * (self.button_width + spacing)
|
self._hovered_item: int = -1
|
||||||
button_rect = rl.Rectangle(button_x, button_y, self.button_width, BUTTON_HEIGHT)
|
self._last_mouse_pos = rl.Vector2(0, 0)
|
||||||
|
|
||||||
# Check button state
|
self._total_height: float = 0
|
||||||
mouse_pos = rl.get_mouse_position()
|
self._visible_range = (0, 0)
|
||||||
is_hovered = rl.check_collision_point_rec(mouse_pos, button_rect)
|
|
||||||
is_pressed = is_hovered and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._is_pressed
|
|
||||||
is_selected = i == self.selected_button
|
|
||||||
|
|
||||||
# Button colors
|
def invalid_height_cache(self):
|
||||||
if is_selected:
|
self._last_dim = (0, 0)
|
||||||
bg_color = rl.Color(51, 171, 76, 255) # Green
|
|
||||||
elif is_pressed:
|
|
||||||
bg_color = rl.Color(74, 74, 74, 255) # Dark gray
|
|
||||||
else:
|
|
||||||
bg_color = rl.Color(57, 57, 57, 255) # Gray
|
|
||||||
|
|
||||||
# Draw button
|
def render(self, rect: rl.Rectangle):
|
||||||
rl.draw_rectangle_rounded(button_rect, 1.0, 20, bg_color)
|
if self._last_dim != (rect.width, rect.height):
|
||||||
|
self._update_item_rects(rect)
|
||||||
|
self._last_dim = (rect.width, rect.height)
|
||||||
|
|
||||||
# Draw text
|
# Update layout and handle scrolling
|
||||||
text_size = measure_text_cached(self._font, text, 40)
|
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._total_height)
|
||||||
text_x = button_x + (self.button_width - text_size.x) / 2
|
scroll_offset = self.scroll_panel.handle_scroll(rect, content_rect)
|
||||||
text_y = button_y + (BUTTON_HEIGHT - text_size.y) / 2
|
|
||||||
rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, rl.Color(228, 228, 228, 255))
|
|
||||||
|
|
||||||
# Handle click
|
# Handle mouse interaction
|
||||||
if is_hovered and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._is_pressed:
|
if self.scroll_panel.is_click_valid():
|
||||||
clicked = i
|
self._handle_mouse_interaction(rect, scroll_offset)
|
||||||
|
|
||||||
if clicked >= 0:
|
# Set scissor mode for clipping
|
||||||
self.selected_button = clicked
|
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
||||||
if self.callback:
|
|
||||||
self.callback(clicked)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
# Calculate visible range for performance
|
||||||
|
self._calculate_visible_range(rect, -scroll_offset.y)
|
||||||
|
|
||||||
class ListItem(Widget):
|
# Render only visible items
|
||||||
def __init__(self, title: str = "", icon: str | None = None, description: str | Callable[[], str] | None = None,
|
for i in range(self._visible_range[0], min(self._visible_range[1], len(self._items))):
|
||||||
description_visible: bool = False, callback: Callable | None = None,
|
item = self._items[i]
|
||||||
action_item: ItemAction | None = None):
|
if item.rect:
|
||||||
super().__init__()
|
adjusted_rect = rl.Rectangle(item.rect.x, item.rect.y + scroll_offset.y, item.rect.width, item.rect.height)
|
||||||
self.title = title
|
self._render_item(item, adjusted_rect, i)
|
||||||
self.icon = icon
|
|
||||||
self.description = description
|
|
||||||
self.description_visible = description_visible
|
|
||||||
self.callback = callback
|
|
||||||
self.action_item = action_item
|
|
||||||
|
|
||||||
self.set_rect(rl.Rectangle(0, 0, ITEM_BASE_WIDTH, ITEM_BASE_HEIGHT))
|
if i != len(self._items) - 1:
|
||||||
self._font = gui_app.font(FontWeight.NORMAL)
|
rl.draw_line_ex(
|
||||||
|
rl.Vector2(adjusted_rect.x + LINE_PADDING, adjusted_rect.y + adjusted_rect.height - 1),
|
||||||
|
rl.Vector2(
|
||||||
|
adjusted_rect.x + adjusted_rect.width - LINE_PADDING * 2, adjusted_rect.y + adjusted_rect.height - 1
|
||||||
|
),
|
||||||
|
1.0,
|
||||||
|
LINE_COLOR,
|
||||||
|
)
|
||||||
|
rl.end_scissor_mode()
|
||||||
|
|
||||||
# Cached properties for performance
|
def _render_item(self, item: ListItem, rect: rl.Rectangle, index: int):
|
||||||
self._prev_max_width: int = 0
|
content_x = rect.x + ITEM_PADDING
|
||||||
self._wrapped_description: str | None = None
|
|
||||||
self._prev_description: str | None = None
|
|
||||||
self._description_height: float = 0
|
|
||||||
|
|
||||||
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
|
|
||||||
super().set_touch_valid_callback(touch_callback)
|
|
||||||
if self.action_item:
|
|
||||||
self.action_item.set_touch_valid_callback(touch_callback)
|
|
||||||
|
|
||||||
def set_parent_rect(self, parent_rect: rl.Rectangle):
|
|
||||||
super().set_parent_rect(parent_rect)
|
|
||||||
self._rect.width = parent_rect.width
|
|
||||||
|
|
||||||
def _handle_mouse_release(self, mouse_pos: rl.Vector2):
|
|
||||||
if not self.is_visible:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check not in action rect
|
|
||||||
if self.action_item:
|
|
||||||
action_rect = self.get_right_item_rect(self._rect)
|
|
||||||
if rl.check_collision_point_rec(mouse_pos, action_rect):
|
|
||||||
# Click was on right item, don't toggle description
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.description:
|
|
||||||
self.description_visible = not self.description_visible
|
|
||||||
content_width = self.get_content_width(int(self._rect.width - ITEM_PADDING * 2))
|
|
||||||
self._rect.height = self.get_item_height(self._font, content_width)
|
|
||||||
|
|
||||||
def _render(self, _):
|
|
||||||
if not self.is_visible:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Don't draw items that are not in parent's viewport
|
|
||||||
if ((self._rect.y + self.rect.height) <= self._parent_rect.y or
|
|
||||||
self._rect.y >= (self._parent_rect.y + self._parent_rect.height)):
|
|
||||||
return
|
|
||||||
|
|
||||||
content_x = self._rect.x + ITEM_PADDING
|
|
||||||
text_x = content_x
|
text_x = content_x
|
||||||
|
|
||||||
# Only draw title and icon for items that have them
|
# Calculate available width for main content
|
||||||
if self.title:
|
content_width = item.get_content_width(int(rect.width - ITEM_PADDING * 2))
|
||||||
# Draw icon if present
|
|
||||||
if self.icon:
|
|
||||||
icon_texture = gui_app.texture(os.path.join("icons", self.icon), ICON_SIZE, ICON_SIZE)
|
|
||||||
rl.draw_texture(icon_texture, int(content_x), int(self._rect.y + (ITEM_BASE_HEIGHT - icon_texture.width) // 2), rl.WHITE)
|
|
||||||
text_x += ICON_SIZE + ITEM_PADDING
|
|
||||||
|
|
||||||
# Draw main text
|
# Draw icon if present
|
||||||
text_size = measure_text_cached(self._font, self.title, ITEM_TEXT_FONT_SIZE)
|
if item.icon:
|
||||||
item_y = self._rect.y + (ITEM_BASE_HEIGHT - text_size.y) // 2
|
icon_texture = gui_app.texture(os.path.join("icons", item.icon), ICON_SIZE, ICON_SIZE)
|
||||||
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), ITEM_TEXT_FONT_SIZE, 0, ITEM_TEXT_COLOR)
|
rl.draw_texture(
|
||||||
|
icon_texture, int(content_x), int(rect.y + (ITEM_BASE_HEIGHT - icon_texture.width) // 2), rl.WHITE
|
||||||
|
)
|
||||||
|
text_x += ICON_SIZE + ITEM_PADDING
|
||||||
|
|
||||||
|
# Draw main text
|
||||||
|
text_size = measure_text_cached(self._font_normal, item.title, ITEM_TEXT_FONT_SIZE)
|
||||||
|
item_y = rect.y + (ITEM_BASE_HEIGHT - text_size.y) // 2
|
||||||
|
rl.draw_text_ex(self._font_normal, item.title, rl.Vector2(text_x, item_y), ITEM_TEXT_FONT_SIZE, 0, ITEM_TEXT_COLOR)
|
||||||
|
|
||||||
|
# Draw description if visible (adjust width for right item)
|
||||||
|
if item.description_visible and item._wrapped_description:
|
||||||
|
desc_y = rect.y + ITEM_DESC_V_OFFSET
|
||||||
|
desc_max_width = int(content_width - (text_x - content_x))
|
||||||
|
|
||||||
|
# Re-wrap description if needed due to right item
|
||||||
|
if (item.right_item and item.description) and not item._wrapped_description:
|
||||||
|
wrapped_lines = wrap_text(self._font_normal, item.description, ITEM_DESC_FONT_SIZE, desc_max_width)
|
||||||
|
item._wrapped_description = "\n".join(wrapped_lines)
|
||||||
|
|
||||||
# Draw description if visible
|
|
||||||
current_description = self.get_description()
|
|
||||||
if self.description_visible and current_description and self._wrapped_description:
|
|
||||||
rl.draw_text_ex(
|
rl.draw_text_ex(
|
||||||
self._font,
|
self._font_normal,
|
||||||
self._wrapped_description,
|
item._wrapped_description,
|
||||||
rl.Vector2(text_x, self._rect.y + ITEM_DESC_V_OFFSET),
|
rl.Vector2(text_x, desc_y),
|
||||||
ITEM_DESC_FONT_SIZE,
|
ITEM_DESC_FONT_SIZE,
|
||||||
0,
|
0,
|
||||||
ITEM_DESC_TEXT_COLOR,
|
ITEM_DESC_TEXT_COLOR,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Draw right item if present
|
# Draw right item if present
|
||||||
if self.action_item:
|
if item.right_item:
|
||||||
right_rect = self.get_right_item_rect(self._rect)
|
right_rect = item.get_right_item_rect(rect)
|
||||||
right_rect.y = self._rect.y
|
# Adjust for scroll offset
|
||||||
if self.action_item.render(right_rect) and self.action_item.enabled:
|
right_rect.y = right_rect.y
|
||||||
|
if item.right_item.draw(right_rect):
|
||||||
# Right item was clicked/activated
|
# Right item was clicked/activated
|
||||||
if self.callback:
|
if item.callback:
|
||||||
self.callback()
|
item.callback()
|
||||||
|
|
||||||
def get_description(self):
|
def _update_item_rects(self, container_rect: rl.Rectangle) -> None:
|
||||||
return _resolve_value(self.description, None)
|
current_y: float = 0.0
|
||||||
|
self._total_height = 0
|
||||||
|
|
||||||
def get_item_height(self, font: rl.Font, max_width: int) -> float:
|
for item in self._items:
|
||||||
if not self.is_visible:
|
content_width = item.get_content_width(int(container_rect.width - ITEM_PADDING * 2))
|
||||||
return 0
|
item_height = item.get_item_height(self._font_normal, content_width)
|
||||||
|
item.rect = rl.Rectangle(container_rect.x, container_rect.y + current_y, container_rect.width, item_height)
|
||||||
|
current_y += item_height
|
||||||
|
self._total_height += item_height
|
||||||
|
|
||||||
current_description = self.get_description()
|
def _calculate_visible_range(self, rect: rl.Rectangle, scroll_offset: float):
|
||||||
if self.description_visible and current_description:
|
if not self._items:
|
||||||
if (
|
self._visible_range = (0, 0)
|
||||||
not self._wrapped_description
|
return
|
||||||
or current_description != self._prev_description
|
|
||||||
or max_width != self._prev_max_width
|
|
||||||
):
|
|
||||||
self._prev_max_width = max_width
|
|
||||||
self._prev_description = current_description
|
|
||||||
|
|
||||||
wrapped_lines = wrap_text(font, current_description, ITEM_DESC_FONT_SIZE, max_width)
|
visible_top = scroll_offset
|
||||||
self._wrapped_description = "\n".join(wrapped_lines)
|
visible_bottom = scroll_offset + rect.height
|
||||||
self._description_height = len(wrapped_lines) * ITEM_DESC_FONT_SIZE + 10
|
|
||||||
return ITEM_BASE_HEIGHT + self._description_height - (ITEM_BASE_HEIGHT - ITEM_DESC_V_OFFSET) + ITEM_PADDING
|
|
||||||
return ITEM_BASE_HEIGHT
|
|
||||||
|
|
||||||
def get_content_width(self, total_width: int) -> int:
|
start_idx = 0
|
||||||
if self.action_item and self.action_item.rect.width > 0:
|
end_idx = len(self._items)
|
||||||
return total_width - int(self.action_item.rect.width) - RIGHT_ITEM_PADDING
|
|
||||||
return total_width
|
|
||||||
|
|
||||||
def get_right_item_rect(self, item_rect: rl.Rectangle) -> rl.Rectangle:
|
# Find first visible item
|
||||||
if not self.action_item:
|
for i, item in enumerate(self._items):
|
||||||
return rl.Rectangle(0, 0, 0, 0)
|
if item.rect and item.rect.y + item.rect.height >= visible_top:
|
||||||
|
start_idx = max(0, i - 1)
|
||||||
|
break
|
||||||
|
|
||||||
right_width = self.action_item.rect.width
|
# Find last visible item
|
||||||
if right_width == 0: # Full width action (like DualButtonAction)
|
for i in range(start_idx, len(self._items)):
|
||||||
return rl.Rectangle(item_rect.x + ITEM_PADDING, item_rect.y,
|
item = self._items[i]
|
||||||
item_rect.width - (ITEM_PADDING * 2), ITEM_BASE_HEIGHT)
|
if item.rect and item.rect.y > visible_bottom:
|
||||||
|
end_idx = min(len(self._items), i + 2)
|
||||||
|
break
|
||||||
|
|
||||||
right_x = item_rect.x + item_rect.width - right_width
|
self._visible_range = (start_idx, end_idx)
|
||||||
right_y = item_rect.y
|
|
||||||
return rl.Rectangle(right_x, right_y, right_width, ITEM_BASE_HEIGHT)
|
def _handle_mouse_interaction(self, rect: rl.Rectangle, scroll_offset: rl.Vector2):
|
||||||
|
mouse_pos = rl.get_mouse_position()
|
||||||
|
|
||||||
|
self._hovered_item = -1
|
||||||
|
if not rl.check_collision_point_rec(mouse_pos, rect):
|
||||||
|
return
|
||||||
|
|
||||||
|
content_mouse_y = mouse_pos.y - rect.y - scroll_offset.y
|
||||||
|
|
||||||
|
for i, item in enumerate(self._items):
|
||||||
|
if item.rect:
|
||||||
|
# Check if mouse is within this item's bounds in content space
|
||||||
|
if (
|
||||||
|
mouse_pos.x >= rect.x
|
||||||
|
and mouse_pos.x <= rect.x + rect.width
|
||||||
|
and content_mouse_y >= item.rect.y
|
||||||
|
and content_mouse_y <= item.rect.y + item.rect.height
|
||||||
|
):
|
||||||
|
item_screen_y = item.rect.y + scroll_offset.y
|
||||||
|
if item_screen_y < rect.height and item_screen_y + item.rect.height > 0:
|
||||||
|
self._hovered_item = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# Handle click on main item (not right item)
|
||||||
|
if rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._hovered_item >= 0:
|
||||||
|
item = self._items[self._hovered_item]
|
||||||
|
|
||||||
|
# Check if click was on right item area
|
||||||
|
if item.right_item and item.rect:
|
||||||
|
adjusted_rect = rl.Rectangle(item.rect.x, item.rect.y + scroll_offset.y, item.rect.width, item.rect.height)
|
||||||
|
right_rect = item.get_right_item_rect(adjusted_rect)
|
||||||
|
if rl.check_collision_point_rec(mouse_pos, right_rect):
|
||||||
|
# Click was handled by right item, don't process main item click
|
||||||
|
return
|
||||||
|
|
||||||
|
# Toggle description visibility if item has description
|
||||||
|
if item.description:
|
||||||
|
item.description_visible = not item.description_visible
|
||||||
|
# Force layout update when description visibility changes
|
||||||
|
self._last_dim = (0, 0)
|
||||||
|
|
||||||
|
# Call item callback
|
||||||
|
if item.callback:
|
||||||
|
item.callback()
|
||||||
|
|
||||||
|
|
||||||
# Factory functions
|
# Factory functions
|
||||||
@@ -338,31 +363,18 @@ def simple_item(title: str, callback: Callable | None = None) -> ListItem:
|
|||||||
return ListItem(title=title, callback=callback)
|
return ListItem(title=title, callback=callback)
|
||||||
|
|
||||||
|
|
||||||
def toggle_item(title: str, description: str | Callable[[], str] | None = None, initial_state: bool = False,
|
def toggle_item(
|
||||||
callback: Callable | None = None, icon: str = "", enabled: bool | Callable[[], bool] = True) -> ListItem:
|
title: str, description: str = None, initial_state: bool = False, callback: Callable | None = None, icon: str = ""
|
||||||
action = ToggleAction(initial_state=initial_state, enabled=enabled)
|
) -> ListItem:
|
||||||
return ListItem(title=title, description=description, action_item=action, icon=icon, callback=callback)
|
toggle = ToggleRightItem(initial_state=initial_state)
|
||||||
|
return ListItem(title=title, description=description, right_item=toggle, icon=icon, callback=callback)
|
||||||
|
|
||||||
|
|
||||||
def button_item(title: str, button_text: str | Callable[[], str], description: str | Callable[[], str] | None = None,
|
def button_item(title: str, button_text: str, description: str = None, callback: Callable | None = None) -> ListItem:
|
||||||
callback: Callable | None = None, enabled: bool | Callable[[], bool] = True) -> ListItem:
|
button = ButtonRightItem(text=button_text)
|
||||||
action = ButtonAction(text=button_text, enabled=enabled)
|
return ListItem(title=title, description=description, right_item=button, callback=callback)
|
||||||
return ListItem(title=title, description=description, action_item=action, callback=callback)
|
|
||||||
|
|
||||||
|
|
||||||
def text_item(title: str, value: str | Callable[[], str], description: str | Callable[[], str] | None = None,
|
def text_item(title: str, value: str, description: str = None, callback: Callable | None = None) -> ListItem:
|
||||||
callback: Callable | None = None, enabled: bool | Callable[[], bool] = True) -> ListItem:
|
text_item = TextRightItem(text=value, color=rl.Color(170, 170, 170, 255))
|
||||||
action = TextAction(text=value, color=rl.Color(170, 170, 170, 255), enabled=enabled)
|
return ListItem(title=title, description=description, right_item=text_item, callback=callback)
|
||||||
return ListItem(title=title, description=description, action_item=action, callback=callback)
|
|
||||||
|
|
||||||
|
|
||||||
def dual_button_item(left_text: str, right_text: str, left_callback: Callable = None, right_callback: Callable = None,
|
|
||||||
description: str | Callable[[], str] | None = None, enabled: bool | Callable[[], bool] = True) -> ListItem:
|
|
||||||
action = DualButtonAction(left_text, right_text, left_callback, right_callback, enabled)
|
|
||||||
return ListItem(title="", description=description, action_item=action)
|
|
||||||
|
|
||||||
|
|
||||||
def multiple_button_item(title: str, description: str, buttons: list[str], selected_index: int,
|
|
||||||
button_width: int = BUTTON_WIDTH, callback: Callable = None, icon: str = ""):
|
|
||||||
action = MultipleButtonAction(buttons, button_width, selected_index, callback=callback)
|
|
||||||
return ListItem(title=title, description=description, icon=icon, action_item=action)
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import pyray as rl
|
import pyray as rl
|
||||||
from collections import deque
|
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
# Scroll constants for smooth scrolling behavior
|
# Scroll constants for smooth scrolling behavior
|
||||||
MOUSE_WHEEL_SCROLL_SPEED = 30
|
MOUSE_WHEEL_SCROLL_SPEED = 30
|
||||||
INERTIA_FRICTION = 0.92 # The rate at which the inertia slows down
|
INERTIA_FRICTION = 0.92 # The rate at which the inertia slows down
|
||||||
MIN_VELOCITY = 0.5 # Minimum velocity before stopping the inertia
|
MIN_VELOCITY = 0.5 # Minimum velocity before stopping the inertia
|
||||||
DRAG_THRESHOLD = 12 # Pixels of movement to consider it a drag, not a click
|
DRAG_THRESHOLD = 5 # Pixels of movement to consider it a drag, not a click
|
||||||
BOUNCE_FACTOR = 0.2 # Elastic bounce when scrolling past boundaries
|
BOUNCE_FACTOR = 0.2 # Elastic bounce when scrolling past boundaries
|
||||||
BOUNCE_RETURN_SPEED = 0.15 # How quickly it returns from the bounce
|
BOUNCE_RETURN_SPEED = 0.15 # How quickly it returns from the bounce
|
||||||
MAX_BOUNCE_DISTANCE = 150 # Maximum distance for bounce effect
|
MAX_BOUNCE_DISTANCE = 150 # Maximum distance for bounce effect
|
||||||
@@ -32,7 +31,8 @@ class GuiScrollPanel:
|
|||||||
self._velocity_y = 0.0 # Velocity for inertia
|
self._velocity_y = 0.0 # Velocity for inertia
|
||||||
self._is_dragging: bool = False
|
self._is_dragging: bool = False
|
||||||
self._bounce_offset: float = 0.0
|
self._bounce_offset: float = 0.0
|
||||||
self._velocity_history: deque[float] = deque(maxlen=VELOCITY_HISTORY_SIZE)
|
self._last_frame_time = rl.get_time()
|
||||||
|
self._velocity_history: list[float] = []
|
||||||
self._last_drag_time: float = 0.0
|
self._last_drag_time: float = 0.0
|
||||||
self._content_rect: rl.Rectangle | None = None
|
self._content_rect: rl.Rectangle | None = None
|
||||||
self._bounds_rect: rl.Rectangle | None = None
|
self._bounds_rect: rl.Rectangle | None = None
|
||||||
@@ -44,6 +44,11 @@ class GuiScrollPanel:
|
|||||||
|
|
||||||
# Calculate time delta
|
# Calculate time delta
|
||||||
current_time = rl.get_time()
|
current_time = rl.get_time()
|
||||||
|
delta_time = current_time - self._last_frame_time
|
||||||
|
self._last_frame_time = current_time
|
||||||
|
|
||||||
|
# Prevent large jumps
|
||||||
|
delta_time = min(delta_time, 0.05)
|
||||||
|
|
||||||
mouse_pos = rl.get_mouse_position()
|
mouse_pos = rl.get_mouse_position()
|
||||||
max_scroll_y = max(content.height - bounds.height, 0)
|
max_scroll_y = max(content.height - bounds.height, 0)
|
||||||
@@ -58,15 +63,13 @@ class GuiScrollPanel:
|
|||||||
if mouse_pos.x >= scrollbar_x:
|
if mouse_pos.x >= scrollbar_x:
|
||||||
self._scroll_state = ScrollState.DRAGGING_SCROLLBAR
|
self._scroll_state = ScrollState.DRAGGING_SCROLLBAR
|
||||||
|
|
||||||
# TODO: hacky
|
|
||||||
# when clicking while moving, go straight into dragging
|
|
||||||
self._is_dragging = abs(self._velocity_y) > MIN_VELOCITY
|
|
||||||
self._last_mouse_y = mouse_pos.y
|
self._last_mouse_y = mouse_pos.y
|
||||||
self._start_mouse_y = mouse_pos.y
|
self._start_mouse_y = mouse_pos.y
|
||||||
self._last_drag_time = current_time
|
self._last_drag_time = current_time
|
||||||
self._velocity_history.clear()
|
self._velocity_history = []
|
||||||
self._velocity_y = 0.0
|
self._velocity_y = 0.0
|
||||||
self._bounce_offset = 0.0
|
self._bounce_offset = 0.0
|
||||||
|
self._is_dragging = False
|
||||||
|
|
||||||
# Handle active dragging
|
# Handle active dragging
|
||||||
if self._scroll_state == ScrollState.DRAGGING_CONTENT or self._scroll_state == ScrollState.DRAGGING_SCROLLBAR:
|
if self._scroll_state == ScrollState.DRAGGING_CONTENT or self._scroll_state == ScrollState.DRAGGING_SCROLLBAR:
|
||||||
@@ -79,6 +82,9 @@ class GuiScrollPanel:
|
|||||||
drag_velocity = delta_y / time_since_last_drag / 60.0
|
drag_velocity = delta_y / time_since_last_drag / 60.0
|
||||||
self._velocity_history.append(drag_velocity)
|
self._velocity_history.append(drag_velocity)
|
||||||
|
|
||||||
|
if len(self._velocity_history) > VELOCITY_HISTORY_SIZE:
|
||||||
|
self._velocity_history.pop(0)
|
||||||
|
|
||||||
self._last_drag_time = current_time
|
self._last_drag_time = current_time
|
||||||
|
|
||||||
# Detect actual dragging
|
# Detect actual dragging
|
||||||
@@ -169,8 +175,13 @@ class GuiScrollPanel:
|
|||||||
|
|
||||||
return self._offset
|
return self._offset
|
||||||
|
|
||||||
def is_touch_valid(self):
|
def is_click_valid(self) -> bool:
|
||||||
return not self._is_dragging
|
# Check if this is a click rather than a drag
|
||||||
|
return (
|
||||||
|
self._scroll_state == ScrollState.IDLE
|
||||||
|
and not self._is_dragging
|
||||||
|
and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
||||||
|
)
|
||||||
|
|
||||||
def get_normalized_scroll_position(self) -> float:
|
def get_normalized_scroll_position(self) -> float:
|
||||||
"""Returns the current scroll position as a value from 0.0 to 1.0"""
|
"""Returns the current scroll position as a value from 0.0 to 1.0"""
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
import pyray as rl
|
|
||||||
from openpilot.system.ui.lib.widget import Widget
|
|
||||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
|
||||||
|
|
||||||
ITEM_SPACING = 40
|
|
||||||
LINE_COLOR = rl.GRAY
|
|
||||||
LINE_PADDING = 40
|
|
||||||
|
|
||||||
|
|
||||||
class LineSeparator(Widget):
|
|
||||||
def __init__(self, height: int = 1):
|
|
||||||
super().__init__()
|
|
||||||
self._rect = rl.Rectangle(0, 0, 0, height)
|
|
||||||
|
|
||||||
def set_parent_rect(self, parent_rect: rl.Rectangle) -> None:
|
|
||||||
super().set_parent_rect(parent_rect)
|
|
||||||
self._rect.width = parent_rect.width
|
|
||||||
|
|
||||||
def _render(self, _):
|
|
||||||
rl.draw_line(int(self._rect.x) + LINE_PADDING, int(self._rect.y),
|
|
||||||
int(self._rect.x + self._rect.width) - LINE_PADDING * 2, int(self._rect.y),
|
|
||||||
LINE_COLOR)
|
|
||||||
|
|
||||||
|
|
||||||
class Scroller(Widget):
|
|
||||||
def __init__(self, items: list[Widget], spacing: int = ITEM_SPACING, line_separator: bool = False, pad_end: bool = True):
|
|
||||||
super().__init__()
|
|
||||||
self._items: list[Widget] = []
|
|
||||||
self._spacing = spacing
|
|
||||||
self._line_separator = line_separator
|
|
||||||
self._pad_end = pad_end
|
|
||||||
|
|
||||||
self.scroll_panel = GuiScrollPanel()
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
self.add_widget(item)
|
|
||||||
|
|
||||||
def add_widget(self, item: Widget) -> None:
|
|
||||||
if self._line_separator and len(self._items) > 0:
|
|
||||||
self._items.append(LineSeparator())
|
|
||||||
self._items.append(item)
|
|
||||||
item.set_touch_valid_callback(self.scroll_panel.is_touch_valid)
|
|
||||||
|
|
||||||
def _render(self, _):
|
|
||||||
# TODO: don't draw items that are not in the viewport
|
|
||||||
visible_items = [item for item in self._items if item.is_visible]
|
|
||||||
content_height = sum(item.rect.height for item in visible_items) + self._spacing * (len(visible_items))
|
|
||||||
if not self._pad_end:
|
|
||||||
content_height -= self._spacing
|
|
||||||
scroll = self.scroll_panel.handle_scroll(self._rect, rl.Rectangle(0, 0, self._rect.width, content_height))
|
|
||||||
|
|
||||||
rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y),
|
|
||||||
int(self._rect.width), int(self._rect.height))
|
|
||||||
|
|
||||||
cur_height = 0
|
|
||||||
for idx, item in enumerate(visible_items):
|
|
||||||
if not item.is_visible:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Nicely lay out items vertically
|
|
||||||
x = self._rect.x
|
|
||||||
y = self._rect.y + cur_height + self._spacing * (idx != 0)
|
|
||||||
cur_height += item.rect.height + self._spacing * (idx != 0)
|
|
||||||
|
|
||||||
# Consider scroll
|
|
||||||
x += scroll.x
|
|
||||||
y += scroll.y
|
|
||||||
|
|
||||||
# Update item state
|
|
||||||
item.set_position(x, y)
|
|
||||||
item.set_parent_rect(self._rect)
|
|
||||||
item.render()
|
|
||||||
|
|
||||||
rl.end_scissor_mode()
|
|
||||||
@@ -1,20 +1,13 @@
|
|||||||
import platform
|
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
MAX_GRADIENT_COLORS = 15
|
MAX_GRADIENT_COLORS = 15
|
||||||
|
|
||||||
VERSION = """
|
FRAGMENT_SHADER = """
|
||||||
#version 300 es
|
#version 300 es
|
||||||
precision highp float;
|
precision mediump float;
|
||||||
"""
|
|
||||||
if platform.system() == "Darwin":
|
|
||||||
VERSION = """
|
|
||||||
#version 330 core
|
|
||||||
"""
|
|
||||||
|
|
||||||
FRAGMENT_SHADER = VERSION + """
|
|
||||||
in vec2 fragTexCoord;
|
in vec2 fragTexCoord;
|
||||||
out vec4 finalColor;
|
out vec4 finalColor;
|
||||||
|
|
||||||
@@ -112,13 +105,14 @@ void main() {
|
|||||||
vec4 color = useGradient == 1 ? getGradientColor(pixel) : fillColor;
|
vec4 color = useGradient == 1 ? getGradientColor(pixel) : fillColor;
|
||||||
finalColor = vec4(color.rgb, color.a * alpha);
|
finalColor = vec4(color.rgb, color.a * alpha);
|
||||||
} else {
|
} else {
|
||||||
discard;
|
finalColor = vec4(0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Default vertex shader
|
# Default vertex shader
|
||||||
VERTEX_SHADER = VERSION + """
|
VERTEX_SHADER = """
|
||||||
|
#version 300 es
|
||||||
in vec3 vertexPosition;
|
in vec3 vertexPosition;
|
||||||
in vec2 vertexTexCoord;
|
in vec2 vertexTexCoord;
|
||||||
out vec2 fragTexCoord;
|
out vec2 fragTexCoord;
|
||||||
@@ -130,6 +124,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
UNIFORM_INT = rl.ShaderUniformDataType.SHADER_UNIFORM_INT
|
UNIFORM_INT = rl.ShaderUniformDataType.SHADER_UNIFORM_INT
|
||||||
UNIFORM_FLOAT = rl.ShaderUniformDataType.SHADER_UNIFORM_FLOAT
|
UNIFORM_FLOAT = rl.ShaderUniformDataType.SHADER_UNIFORM_FLOAT
|
||||||
UNIFORM_VEC2 = rl.ShaderUniformDataType.SHADER_UNIFORM_VEC2
|
UNIFORM_VEC2 = rl.ShaderUniformDataType.SHADER_UNIFORM_VEC2
|
||||||
@@ -249,7 +244,6 @@ def _configure_shader_color(state, color, gradient, clipped_rect, original_rect)
|
|||||||
state.fill_color_ptr[0:4] = [color.r / 255.0, color.g / 255.0, color.b / 255.0, color.a / 255.0]
|
state.fill_color_ptr[0:4] = [color.r / 255.0, color.g / 255.0, color.b / 255.0, color.a / 255.0]
|
||||||
rl.set_shader_value(state.shader, state.locations['fillColor'], state.fill_color_ptr, UNIFORM_VEC4)
|
rl.set_shader_value(state.shader, state.locations['fillColor'], state.fill_color_ptr, UNIFORM_VEC4)
|
||||||
|
|
||||||
|
|
||||||
def draw_polygon(origin_rect: rl.Rectangle, points: np.ndarray, color=None, gradient=None):
|
def draw_polygon(origin_rect: rl.Rectangle, points: np.ndarray, color=None, gradient=None):
|
||||||
"""
|
"""
|
||||||
Draw a complex polygon using shader-based even-odd fill rule
|
Draw a complex polygon using shader-based even-odd fill rule
|
||||||
|
|||||||
+14
-11
@@ -1,5 +1,4 @@
|
|||||||
import pyray as rl
|
import pyray as rl
|
||||||
from openpilot.system.ui.lib.widget import Widget
|
|
||||||
|
|
||||||
ON_COLOR = rl.Color(51, 171, 76, 255)
|
ON_COLOR = rl.Color(51, 171, 76, 255)
|
||||||
OFF_COLOR = rl.Color(0x39, 0x39, 0x39, 255)
|
OFF_COLOR = rl.Color(0x39, 0x39, 0x39, 255)
|
||||||
@@ -12,23 +11,24 @@ BG_HEIGHT = 60
|
|||||||
ANIMATION_SPEED = 8.0
|
ANIMATION_SPEED = 8.0
|
||||||
|
|
||||||
|
|
||||||
class Toggle(Widget):
|
class Toggle:
|
||||||
def __init__(self, initial_state=False):
|
def __init__(self, initial_state=False):
|
||||||
super().__init__()
|
|
||||||
self._state = initial_state
|
self._state = initial_state
|
||||||
self._enabled = True
|
self._enabled = True
|
||||||
|
self._rect = rl.Rectangle(0, 0, WIDTH, HEIGHT)
|
||||||
self._progress = 1.0 if initial_state else 0.0
|
self._progress = 1.0 if initial_state else 0.0
|
||||||
self._target = self._progress
|
self._target = self._progress
|
||||||
|
|
||||||
def set_rect(self, rect: rl.Rectangle):
|
def handle_input(self):
|
||||||
self._rect = rl.Rectangle(rect.x, rect.y, WIDTH, HEIGHT)
|
|
||||||
|
|
||||||
def _handle_mouse_release(self, mouse_pos: rl.Vector2):
|
|
||||||
if not self._enabled:
|
if not self._enabled:
|
||||||
return
|
return 0
|
||||||
|
|
||||||
self._state = not self._state
|
if rl.is_mouse_button_pressed(rl.MOUSE_LEFT_BUTTON):
|
||||||
self._target = 1.0 if self._state else 0.0
|
if rl.check_collision_point_rec(rl.get_mouse_position(), self._rect):
|
||||||
|
self._state = not self._state
|
||||||
|
self._target = 1.0 if self._state else 0.0
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
def get_state(self):
|
def get_state(self):
|
||||||
return self._state
|
return self._state
|
||||||
@@ -49,7 +49,8 @@ class Toggle(Widget):
|
|||||||
self._progress += delta if self._progress < self._target else -delta
|
self._progress += delta if self._progress < self._target else -delta
|
||||||
self._progress = max(0.0, min(1.0, self._progress))
|
self._progress = max(0.0, min(1.0, self._progress))
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle):
|
def render(self, rect: rl.Rectangle):
|
||||||
|
self._rect.x, self._rect.y = rect.x, rect.y
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
if self._enabled:
|
if self._enabled:
|
||||||
@@ -68,5 +69,7 @@ class Toggle(Widget):
|
|||||||
knob_y = self._rect.y + HEIGHT / 2
|
knob_y = self._rect.y + HEIGHT / 2
|
||||||
rl.draw_circle(int(knob_x), int(knob_y), HEIGHT / 2, knob_color)
|
rl.draw_circle(int(knob_x), int(knob_y), HEIGHT / 2, knob_color)
|
||||||
|
|
||||||
|
return self.handle_input()
|
||||||
|
|
||||||
def _blend_color(self, c1, c2, t):
|
def _blend_color(self, c1, c2, t):
|
||||||
return rl.Color(int(c1.r + (c2.r - c1.r) * t), int(c1.g + (c2.g - c1.g) * t), int(c1.b + (c2.b - c1.b) * t), 255)
|
return rl.Color(int(c1.r + (c2.r - c1.r) * t), int(c1.g + (c2.g - c1.g) * t), int(c1.b + (c2.b - c1.b) * t), 255)
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
import abc
|
|
||||||
import pyray as rl
|
|
||||||
from enum import IntEnum
|
|
||||||
from collections.abc import Callable
|
|
||||||
|
|
||||||
|
|
||||||
class DialogResult(IntEnum):
|
|
||||||
CANCEL = 0
|
|
||||||
CONFIRM = 1
|
|
||||||
NO_ACTION = -1
|
|
||||||
|
|
||||||
|
|
||||||
class Widget(abc.ABC):
|
|
||||||
def __init__(self):
|
|
||||||
self._rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
|
|
||||||
self._parent_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
|
|
||||||
self._is_pressed = False
|
|
||||||
self._is_visible: bool | Callable[[], bool] = True
|
|
||||||
self._touch_valid_callback: Callable[[], bool] | None = None
|
|
||||||
|
|
||||||
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
|
|
||||||
"""Set a callback to determine if the widget can be clicked."""
|
|
||||||
self._touch_valid_callback = touch_callback
|
|
||||||
|
|
||||||
def _touch_valid(self) -> bool:
|
|
||||||
"""Check if the widget can be touched."""
|
|
||||||
return self._touch_valid_callback() if self._touch_valid_callback else True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_visible(self) -> bool:
|
|
||||||
return self._is_visible() if callable(self._is_visible) else self._is_visible
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rect(self) -> rl.Rectangle:
|
|
||||||
return self._rect
|
|
||||||
|
|
||||||
def set_visible(self, visible: bool | Callable[[], bool]) -> None:
|
|
||||||
self._is_visible = visible
|
|
||||||
|
|
||||||
def set_rect(self, rect: rl.Rectangle) -> None:
|
|
||||||
changed = (self._rect.x != rect.x or self._rect.y != rect.y or
|
|
||||||
self._rect.width != rect.width or self._rect.height != rect.height)
|
|
||||||
self._rect = rect
|
|
||||||
if changed:
|
|
||||||
self._update_layout_rects()
|
|
||||||
|
|
||||||
def set_parent_rect(self, parent_rect: rl.Rectangle) -> None:
|
|
||||||
"""Can be used like size hint in QT"""
|
|
||||||
self._parent_rect = parent_rect
|
|
||||||
|
|
||||||
def set_position(self, x: float, y: float) -> None:
|
|
||||||
changed = (self._rect.x != x or self._rect.y != y)
|
|
||||||
self._rect.x, self._rect.y = x, y
|
|
||||||
if changed:
|
|
||||||
self._update_layout_rects()
|
|
||||||
|
|
||||||
def render(self, rect: rl.Rectangle = None) -> bool | int | None:
|
|
||||||
if rect is not None:
|
|
||||||
self.set_rect(rect)
|
|
||||||
|
|
||||||
self._update_state()
|
|
||||||
|
|
||||||
if not self.is_visible:
|
|
||||||
return None
|
|
||||||
|
|
||||||
ret = self._render(self._rect)
|
|
||||||
|
|
||||||
# Keep track of whether mouse down started within the widget's rectangle
|
|
||||||
mouse_pos = rl.get_mouse_position()
|
|
||||||
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._touch_valid():
|
|
||||||
if rl.check_collision_point_rec(mouse_pos, self._rect):
|
|
||||||
self._is_pressed = True
|
|
||||||
|
|
||||||
elif not self._touch_valid():
|
|
||||||
self._is_pressed = False
|
|
||||||
|
|
||||||
elif rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
|
||||||
if self._is_pressed and rl.check_collision_point_rec(mouse_pos, self._rect):
|
|
||||||
self._handle_mouse_release(mouse_pos)
|
|
||||||
self._is_pressed = False
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _render(self, rect: rl.Rectangle) -> bool | int | None:
|
|
||||||
"""Render the widget within the given rectangle."""
|
|
||||||
|
|
||||||
def _update_state(self):
|
|
||||||
"""Optionally update the widget's non-layout state. This is called before rendering."""
|
|
||||||
|
|
||||||
def _update_layout_rects(self) -> None:
|
|
||||||
"""Optionally update any layout rects on Widget rect change."""
|
|
||||||
|
|
||||||
def _handle_mouse_release(self, mouse_pos: rl.Vector2) -> bool:
|
|
||||||
"""Optionally handle mouse release events."""
|
|
||||||
return False
|
|
||||||
@@ -13,7 +13,6 @@ from dbus_next.aio import MessageBus
|
|||||||
from dbus_next import BusType, Variant, Message
|
from dbus_next import BusType, Variant, Message
|
||||||
from dbus_next.errors import DBusError
|
from dbus_next.errors import DBusError
|
||||||
from dbus_next.constants import MessageType
|
from dbus_next.constants import MessageType
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -39,7 +38,6 @@ NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8
|
|||||||
TETHERING_IP_ADDRESS = "192.168.43.1"
|
TETHERING_IP_ADDRESS = "192.168.43.1"
|
||||||
DEFAULT_TETHERING_PASSWORD = "12345678"
|
DEFAULT_TETHERING_PASSWORD = "12345678"
|
||||||
|
|
||||||
|
|
||||||
# NetworkManager device states
|
# NetworkManager device states
|
||||||
class NMDeviceState(IntEnum):
|
class NMDeviceState(IntEnum):
|
||||||
DISCONNECTED = 30
|
DISCONNECTED = 30
|
||||||
@@ -48,7 +46,6 @@ class NMDeviceState(IntEnum):
|
|||||||
IP_CONFIG = 70
|
IP_CONFIG = 70
|
||||||
ACTIVATED = 100
|
ACTIVATED = 100
|
||||||
|
|
||||||
|
|
||||||
class SecurityType(IntEnum):
|
class SecurityType(IntEnum):
|
||||||
OPEN = 0
|
OPEN = 0
|
||||||
WPA = 1
|
WPA = 1
|
||||||
@@ -56,7 +53,6 @@ class SecurityType(IntEnum):
|
|||||||
WPA3 = 3
|
WPA3 = 3
|
||||||
UNSUPPORTED = 4
|
UNSUPPORTED = 4
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class NetworkInfo:
|
class NetworkInfo:
|
||||||
ssid: str
|
ssid: str
|
||||||
@@ -231,7 +227,7 @@ class WifiManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._current_connection_ssid = None
|
self._current_connection_ssid = None
|
||||||
cloudlog.error(f"Error connecting to network: {e}")
|
cloudlog.error(f"Error connecting to network: {e}")
|
||||||
# Notify UI of failure
|
# Notify UI of failure
|
||||||
if self.callbacks.connection_failed:
|
if self.callbacks.connection_failed:
|
||||||
self.callbacks.connection_failed(ssid, str(e))
|
self.callbacks.connection_failed(ssid, str(e))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
from typing import Generic, Protocol, TypeVar
|
||||||
|
from openpilot.common.swaglog import cloudlog
|
||||||
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
|
|
||||||
|
|
||||||
|
class RendererProtocol(Protocol):
|
||||||
|
def render(self): ...
|
||||||
|
|
||||||
|
|
||||||
|
R = TypeVar("R", bound=RendererProtocol)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseWindow(Generic[R]):
|
||||||
|
def __init__(self, title: str):
|
||||||
|
self._title = title
|
||||||
|
self._renderer: R | None = None
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
self._thread = threading.Thread(target=self._run)
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
# wait for the renderer to be initialized
|
||||||
|
while self._renderer is None and self._thread.is_alive():
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
def _create_renderer(self) -> R:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
if os.getenv("CI") is not None:
|
||||||
|
return
|
||||||
|
gui_app.init_window(self._title)
|
||||||
|
self._renderer = self._create_renderer()
|
||||||
|
try:
|
||||||
|
for _ in gui_app.render():
|
||||||
|
if self._stop_event.is_set():
|
||||||
|
break
|
||||||
|
self._renderer.render()
|
||||||
|
finally:
|
||||||
|
gui_app.close()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self._thread.is_alive():
|
||||||
|
self._stop_event.set()
|
||||||
|
self._thread.join(timeout=2.0)
|
||||||
|
if self._thread.is_alive():
|
||||||
|
cloudlog.warning(f"Failed to join {self._title} thread")
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.close()
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user