mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-11 22:14:46 +08:00
Compare commits
121 Commits
clsuter-ve
...
dockerize-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be63625fa8 | ||
|
|
cdf990c1b1 | ||
|
|
ea8eaed1aa | ||
|
|
4e094bc740 | ||
|
|
17432e1b0d | ||
|
|
0218ae82ed | ||
|
|
7b35f64049 | ||
|
|
0a254fbc4e | ||
|
|
903f426bb9 | ||
|
|
53d757a84f | ||
|
|
fa5fce465a | ||
|
|
d1893ee3eb | ||
|
|
56fca1353f | ||
|
|
a22eecd773 | ||
|
|
01b3f70c01 | ||
|
|
8b8f33f488 | ||
|
|
d5b5383f1a | ||
|
|
0cfdd6757b | ||
|
|
91792aa767 | ||
|
|
c1e0b87059 | ||
|
|
7f6f346c38 | ||
|
|
5e3fc13751 | ||
|
|
885f3f73e0 | ||
|
|
2c78cfe200 | ||
|
|
4a4f3fce94 | ||
|
|
5772683432 | ||
|
|
6a37d8a89e | ||
|
|
87a6e369aa | ||
|
|
d62c3cdef9 | ||
|
|
5f3d876aaa | ||
|
|
5f559cfcc7 | ||
|
|
42fc89a0e5 | ||
|
|
ccd55d3663 | ||
|
|
25f5ec46d9 | ||
|
|
c460f5150f | ||
|
|
b18037c38a | ||
|
|
6b9c63acbe | ||
|
|
b5d5fa755f | ||
|
|
f9792fe717 | ||
|
|
03f3d6ccf1 | ||
|
|
4eb64561f2 | ||
|
|
762f11c620 | ||
|
|
2a9e35609b | ||
|
|
6352589902 | ||
|
|
7293a19472 | ||
|
|
f4df569064 | ||
|
|
2706179f84 | ||
|
|
25e123a23a | ||
|
|
f275d6d892 | ||
|
|
62b301ae76 | ||
|
|
2a1939f37a | ||
|
|
7b8d6b6eb7 | ||
|
|
e9fe40755c | ||
|
|
cd657f35f0 | ||
|
|
98c34c4b7d | ||
|
|
3a10bdb1e7 | ||
|
|
5138217673 | ||
|
|
32ae9efb3d | ||
|
|
723a52626d | ||
|
|
f3d0a9ea13 | ||
|
|
9d8e4acec9 | ||
|
|
79319d2447 | ||
|
|
58763f4551 | ||
|
|
fcebb5eb9f | ||
|
|
f7ce5fb94c | ||
|
|
1562b88f63 | ||
|
|
3d987cb9b5 | ||
|
|
0cb0bf8028 | ||
|
|
e345f25ce4 | ||
|
|
03d2e7b2b0 | ||
|
|
5ebbb46fdf | ||
|
|
2017bf970f | ||
|
|
c1794e6f83 | ||
|
|
a9e8649137 | ||
|
|
bfa3f3cccb | ||
|
|
d9b6c16037 | ||
|
|
75b6ec68c6 | ||
|
|
1c11e28448 | ||
|
|
14166c980e | ||
|
|
61b8f6f478 | ||
|
|
d3b300a148 | ||
|
|
ffb677b53d | ||
|
|
fc27423ac2 | ||
|
|
08aeeabc9b | ||
|
|
e015e319b7 | ||
|
|
41db89afdc | ||
|
|
f70592b7e9 | ||
|
|
9153f97900 | ||
|
|
7b4e2e2430 | ||
|
|
9a1e58102d | ||
|
|
5df875390f | ||
|
|
0e2f69883b | ||
|
|
f824e6c0ec | ||
|
|
191d0d429e | ||
|
|
af48d23a68 | ||
|
|
0c6856cf03 | ||
|
|
e93a7234bc | ||
|
|
ce93a7215d | ||
|
|
f0f249ecf8 | ||
|
|
a3daca8fd5 | ||
|
|
6d09b2405e | ||
|
|
8220599dd8 | ||
|
|
7c5155590f | ||
|
|
e0a2a7af64 | ||
|
|
9a2ec552f1 | ||
|
|
2c59b5f8c6 | ||
|
|
db5e413049 | ||
|
|
2031a33188 | ||
|
|
7875cc4713 | ||
|
|
c3aa7cffed | ||
|
|
c145de96f9 | ||
|
|
4bbbf51236 | ||
|
|
3ce87d0ac9 | ||
|
|
a1ee5f5ba8 | ||
|
|
29830440b4 | ||
|
|
541bd4d4d9 | ||
|
|
6767bfce44 | ||
|
|
75434b10b9 | ||
|
|
63e7a0ca15 | ||
|
|
ba2d2677c1 | ||
|
|
e389b19ed7 |
@@ -18,6 +18,19 @@
|
||||
|
||||
venv/
|
||||
.venv/
|
||||
**/.idea
|
||||
**/.hypothesis
|
||||
**/.mypy_cache
|
||||
|
||||
**/.venv
|
||||
**/.venv/
|
||||
|
||||
**/.ci_cache
|
||||
**/*.rlog
|
||||
|
||||
**/Dockerfile*
|
||||
**/dockerfile*
|
||||
**/build_output
|
||||
|
||||
notebooks
|
||||
phone
|
||||
|
||||
3
.github/workflows/repo-maintenance.yaml
vendored
3
.github/workflows/repo-maintenance.yaml
vendored
@@ -54,8 +54,9 @@ jobs:
|
||||
git add .
|
||||
- name: update car docs
|
||||
run: |
|
||||
export PYTHONPATH="$PWD"
|
||||
scons -j$(nproc) --minimal opendbc_repo
|
||||
PYTHONPATH=. python selfdrive/car/docs.py
|
||||
python selfdrive/car/docs.py
|
||||
git add docs/CARS.md
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
|
||||
|
||||
@@ -3,7 +3,6 @@ FROM ghcr.io/commaai/openpilot-base:latest
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
ENV OPENPILOT_PATH=/home/batman/openpilot
|
||||
ENV PYTHONPATH=${OPENPILOT_PATH}:${PYTHONPATH}
|
||||
|
||||
RUN mkdir -p ${OPENPILOT_PATH}
|
||||
WORKDIR ${OPENPILOT_PATH}
|
||||
|
||||
66
Dockerfile.sunnypilot
Normal file
66
Dockerfile.sunnypilot
Normal file
@@ -0,0 +1,66 @@
|
||||
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,6 +2281,7 @@ struct LiveTorqueParametersData {
|
||||
points @10 :List(List(Float32));
|
||||
version @11 :Int32;
|
||||
useParams @12 :Bool;
|
||||
calPerc @13 :Int8;
|
||||
}
|
||||
|
||||
struct LiveDelayData {
|
||||
@@ -2291,6 +2292,7 @@ struct LiveDelayData {
|
||||
lateralDelayEstimate @3 :Float32;
|
||||
lateralDelayEstimateStd @5 :Float32;
|
||||
points @4 :List(Float32);
|
||||
calPerc @6 :Int8;
|
||||
|
||||
enum Status {
|
||||
unestimated @0;
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define DEFAULT_MODEL "Filet o Fish (Default)"
|
||||
#define DEFAULT_MODEL "Vegetarian Filet o Fish (Default)"
|
||||
|
||||
@@ -131,6 +131,9 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"CarParamsSPCache", CLEAR_ON_MANAGER_START},
|
||||
{"CarParamsSPPersistent", PERSISTENT},
|
||||
{"CarPlatformBundle", PERSISTENT},
|
||||
{"CustomAccIncrementsEnabled", PERSISTENT | BACKUP},
|
||||
{"CustomAccLongPressIncrement", PERSISTENT | BACKUP},
|
||||
{"CustomAccShortPressIncrement", PERSISTENT | BACKUP},
|
||||
{"DeviceBootMode", PERSISTENT | BACKUP},
|
||||
{"EnableGithubRunner", PERSISTENT | BACKUP},
|
||||
{"MaxTimeOffroad", PERSISTENT | BACKUP},
|
||||
|
||||
52
common/spinner.py
Executable file
52
common/spinner.py
Executable file
@@ -0,0 +1,52 @@
|
||||
import os
|
||||
import subprocess
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
|
||||
|
||||
class Spinner:
|
||||
def __init__(self):
|
||||
try:
|
||||
self.spinner_proc = subprocess.Popen(["./spinner.py"],
|
||||
stdin=subprocess.PIPE,
|
||||
cwd=os.path.join(BASEDIR, "system", "ui"),
|
||||
close_fds=True)
|
||||
except OSError:
|
||||
self.spinner_proc = None
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def update(self, spinner_text: str):
|
||||
if self.spinner_proc is not None:
|
||||
self.spinner_proc.stdin.write(spinner_text.encode('utf8') + b"\n")
|
||||
try:
|
||||
self.spinner_proc.stdin.flush()
|
||||
except BrokenPipeError:
|
||||
pass
|
||||
|
||||
def update_progress(self, cur: float, total: float):
|
||||
self.update(str(round(100 * cur / total)))
|
||||
|
||||
def close(self):
|
||||
if self.spinner_proc is not None:
|
||||
self.spinner_proc.kill()
|
||||
try:
|
||||
self.spinner_proc.communicate(timeout=2.)
|
||||
except subprocess.TimeoutExpired:
|
||||
print("WARNING: failed to kill spinner")
|
||||
self.spinner_proc = None
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import time
|
||||
with Spinner() as s:
|
||||
s.update("Spinner text")
|
||||
time.sleep(5.0)
|
||||
print("gone")
|
||||
time.sleep(5.0)
|
||||
63
common/text_window.py
Executable file
63
common/text_window.py
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
|
||||
|
||||
class TextWindow:
|
||||
def __init__(self, text):
|
||||
try:
|
||||
self.text_proc = subprocess.Popen(["./text.py", text],
|
||||
stdin=subprocess.PIPE,
|
||||
cwd=os.path.join(BASEDIR, "system", "ui"),
|
||||
close_fds=True)
|
||||
except OSError:
|
||||
self.text_proc = None
|
||||
|
||||
def get_status(self):
|
||||
if self.text_proc is not None:
|
||||
self.text_proc.poll()
|
||||
return self.text_proc.returncode
|
||||
return None
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def close(self):
|
||||
if self.text_proc is not None:
|
||||
self.text_proc.terminate()
|
||||
self.text_proc = None
|
||||
|
||||
def wait_for_exit(self):
|
||||
if self.text_proc is not None:
|
||||
while True:
|
||||
if self.get_status() == 1:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
text = """Traceback (most recent call last):
|
||||
File "./controlsd.py", line 608, in <module>
|
||||
main()
|
||||
File "./controlsd.py", line 604, in main
|
||||
controlsd_thread(sm, pm, logcan)
|
||||
File "./controlsd.py", line 455, in controlsd_thread
|
||||
1/0
|
||||
ZeroDivisionError: division by zero"""
|
||||
print(text)
|
||||
|
||||
with TextWindow(text) as s:
|
||||
for _ in range(100):
|
||||
if s.get_status() == 1:
|
||||
print("Got exit button")
|
||||
break
|
||||
time.sleep(0.1)
|
||||
print("gone")
|
||||
16
docs/CARS.md
16
docs/CARS.md
@@ -15,7 +15,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=A3 2014-19">Buy Here</a></sub></details>|||
|
||||
|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=A3 Sportback e-tron 2017-18">Buy Here</a></sub></details>|||
|
||||
|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=Q2 2018">Buy Here</a></sub></details>|||
|
||||
|Audi|Q3 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=Q3 2019-23">Buy Here</a></sub></details>|||
|
||||
|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=Q3 2019-24">Buy Here</a></sub></details>|||
|
||||
|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=RS3 2018">Buy Here</a></sub></details>|||
|
||||
|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=S3 2015-17">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
@@ -38,6 +38,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape Hybrid 2023-24">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|
||||
|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape Plug-in Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||
|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape Plug-in Hybrid 2023-24">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|
||||
|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Expedition 2022-24">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=MewJc9LYp9M|
|
||||
|Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Explorer 2020-24">Buy Here</a></sub></details>|||
|
||||
|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Explorer Hybrid 2020-24">Buy Here</a></sub></details>|||
|
||||
|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=F-150 2021-23">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=MewJc9LYp9M|
|
||||
@@ -53,7 +54,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Ford|Maverick 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick 2023-24">Buy Here</a></sub></details>|||
|
||||
|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick Hybrid 2022">Buy Here</a></sub></details>|||
|
||||
|Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick Hybrid 2023-24">Buy Here</a></sub></details>|||
|
||||
|Ford|Mustang Mach-E 2021-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Mustang Mach-E 2021-23">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|
||||
|Ford|Mustang Mach-E 2021-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Mustang Mach-E 2021-24">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|
||||
|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Ranger 2024">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|
||||
|Genesis|G70 2018|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G70 2018">Buy Here</a></sub></details>|||
|
||||
|Genesis|G70 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G70 2019-21">Buy Here</a></sub></details>|||
|
||||
@@ -77,8 +78,8 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Honda|Civic 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic Hatchback 2017-21|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback 2017-21">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic Hatchback Hybrid 2023 (Europe only)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback Hybrid 2023 (Europe only)">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback Hybrid 2025|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback Hybrid 2025">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback Hybrid (Europe only) 2023">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V 2015-16|Touring Trim|openpilot|26 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V 2015-16">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V 2017-22">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V Hybrid 2017-22">Buy Here</a></sub></details>|||
|
||||
@@ -115,7 +116,6 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Hyundai|Ioniq Plug-in Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|32 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Ioniq Plug-in Hybrid 2019">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Ioniq Plug-in Hybrid 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Ioniq Plug-in Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona 2020|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|6 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona 2020">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona 2022|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai O connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona 2022">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona Electric 2018-21">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai O connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona Electric 2022-23">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona Electric (with HDA II, Korea only) 2023[<sup>6</sup>](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai R connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona Electric (with HDA II, Korea only) 2023">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=U2fOCmcQ8hw" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
@@ -181,12 +181,12 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Kia|Telluride 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Kia&model=Telluride 2020-22">Buy Here</a></sub></details>|||
|
||||
|Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=CT Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=ES 2017-18">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES 2019-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=ES 2019-24">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=ES 2019-25">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES Hybrid 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=ES Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=ES Hybrid 2019-25">Buy Here</a></sub></details>|<a href="https://youtu.be/BZ29osRVJeg?t=12" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Lexus|GS F 2016|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=GS F 2016">Buy Here</a></sub></details>|||
|
||||
|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=IS 2017-19">Buy Here</a></sub></details>|||
|
||||
|Lexus|IS 2022-23|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=IS 2022-23">Buy Here</a></sub></details>|||
|
||||
|Lexus|IS 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=IS 2022-24">Buy Here</a></sub></details>|||
|
||||
|Lexus|LC 2024|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=LC 2024">Buy Here</a></sub></details>|||
|
||||
|Lexus|NX 2018-19|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX 2018-19">Buy Here</a></sub></details>|||
|
||||
|Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX 2020-21">Buy Here</a></sub></details>|||
|
||||
@@ -313,11 +313,11 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Polo GTI 2018-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
||||
|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=T-Cross 2021">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
||||
|Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=T-Roc 2018-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Taos 2022-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Taos 2022-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Taos 2022-24">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Teramont 2018-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Teramont Cross Sport 2021-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Teramont X 2021-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Tiguan 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Tiguan 2018-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Tiguan 2018-24">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Tiguan eHybrid 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Touran 2016-23">Buy Here</a></sub></details>|||
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ Development is coordinated through [Discord](https://discord.comma.ai) and GitHu
|
||||
### Getting Started
|
||||
|
||||
* Setup your [development environment](../tools/)
|
||||
* Read about the [development workflow](WORKFLOW.md)
|
||||
* Join our [Discord](https://discord.comma.ai)
|
||||
* Docs are at https://docs.comma.ai and https://blog.comma.ai
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# openpilot development workflow
|
||||
|
||||
Aside from the ML models, most tools used for openpilot development are in this repo.
|
||||
|
||||
Most development happens on normal Ubuntu workstations, and not in cars or directly on comma devices. See the [setup guide](../tools) for getting your PC setup for openpilot development.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# get the latest stuff
|
||||
git pull
|
||||
git lfs pull
|
||||
git submodule update --init --recursive
|
||||
|
||||
# update dependencies
|
||||
tools/ubuntu_setup.sh
|
||||
|
||||
# build everything
|
||||
scons -j$(nproc)
|
||||
|
||||
# build just the ui with either of these
|
||||
scons -j8 selfdrive/ui/
|
||||
cd selfdrive/ui/ && scons -u -j8
|
||||
|
||||
# test everything
|
||||
pytest
|
||||
|
||||
# test just logging services
|
||||
cd system/loggerd && pytest .
|
||||
|
||||
# run the linter
|
||||
op lint
|
||||
```
|
||||
@@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1
|
||||
export VECLIB_MAXIMUM_THREADS=1
|
||||
|
||||
if [ -z "$AGNOS_VERSION" ]; then
|
||||
export AGNOS_VERSION="12.3"
|
||||
export AGNOS_VERSION="12.4"
|
||||
fi
|
||||
|
||||
export STAGING_ROOT="/data/safe_staging"
|
||||
|
||||
Submodule opendbc_repo updated: 3bd2bba8a7...c87940a6b5
2
panda
2
panda
Submodule panda updated: 5ac4fa5bb0...c33cfa0803
@@ -68,6 +68,9 @@ dependencies = [
|
||||
|
||||
# logreader
|
||||
"zstandard",
|
||||
|
||||
# ui
|
||||
"qrcode",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
@@ -85,7 +88,8 @@ testing = [
|
||||
"pytest-cov",
|
||||
"pytest-cpp",
|
||||
"pytest-subtests",
|
||||
"pytest-xdist",
|
||||
# https://github.com/pytest-dev/pytest-xdist/issues/1215
|
||||
"pytest-xdist @ git+https://github.com/sshane/pytest-xdist@909e97b49d12401c10608f9d777bfc9dab8a4413",
|
||||
"pytest-timeout",
|
||||
"pytest-randomly",
|
||||
"pytest-asyncio",
|
||||
@@ -102,8 +106,8 @@ dev = [
|
||||
"azure-storage-blob",
|
||||
"dbus-next",
|
||||
"dictdiffer",
|
||||
"lru-dict",
|
||||
"matplotlib",
|
||||
"opencv-python-headless",
|
||||
"parameterized >=0.8, <0.9",
|
||||
"pyautogui",
|
||||
"pygame",
|
||||
|
||||
Submodule rednose_repo updated: 8b86052919...7fddc8e6d4
@@ -1,36 +1,31 @@
|
||||
# openpilot releases
|
||||
|
||||
```
|
||||
## release checklist
|
||||
|
||||
**Go to `devel-staging`**
|
||||
- [ ] update RELEASES.md
|
||||
- [ ] update `devel-staging`: `git reset --hard origin/master-ci`
|
||||
- [ ] open a pull request from `devel-staging` to `devel`
|
||||
- [ ] post on Discord
|
||||
|
||||
**Go to `devel`**
|
||||
- [ ] update RELEASES.md
|
||||
- [ ] close out milestone
|
||||
- [ ] post on Discord dev channel
|
||||
- [ ] bump version on master: `common/version.h` and `RELEASES.md`
|
||||
- [ ] merge the pull request
|
||||
|
||||
tests:
|
||||
- [ ] update from previous release -> new release
|
||||
- [ ] update from new release -> previous release
|
||||
- [ ] fresh install with `openpilot-test.comma.ai`
|
||||
- [ ] drive on fresh install
|
||||
- [ ] comma body test
|
||||
- [ ] no submodules or LFS
|
||||
- [ ] check sentry, MTBF, etc.
|
||||
- [ ] before merging the pull request
|
||||
- [ ] update from previous release -> new release
|
||||
- [ ] update from new release -> previous release
|
||||
- [ ] fresh install with `openpilot-test.comma.ai`
|
||||
- [ ] drive on fresh install
|
||||
- [ ] no submodules or LFS
|
||||
- [ ] check sentry, MTBF, etc.
|
||||
|
||||
**Go to `release3`**
|
||||
- [ ] publish the blog post
|
||||
- [ ] `git reset --hard origin/release3-staging`
|
||||
- [ ] tag the release
|
||||
```
|
||||
git tag v0.X.X <commit-hash>
|
||||
git push origin v0.X.X
|
||||
```
|
||||
- [ ] tag the release: `git tag v0.X.X <commit-hash> && git push origin v0.X.X`
|
||||
- [ ] create GitHub release
|
||||
- [ ] final test install on `openpilot.comma.ai`
|
||||
- [ ] update production
|
||||
- [ ] Post on Discord, X, etc.
|
||||
- [ ] update factory provisioning
|
||||
- [ ] close out milestone
|
||||
- [ ] post on Discord, X, etc.
|
||||
```
|
||||
|
||||
51
release/ci/docker_build_sp.sh
Normal file
51
release/ci/docker_build_sp.sh
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/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,11 +12,10 @@
|
||||
|
||||
<h5>Quectel/EG25-G</h5>
|
||||
<p>FCC ID: XMR201903EG25G</p>
|
||||
<p>
|
||||
This device complies with Part 15 of the FCC Rules.
|
||||
Operation is subject to the following two conditions:
|
||||
<p>This device complies with Part 15 of the FCC Rules.</p>
|
||||
<p>Operation is subject to the following two conditions:</p>
|
||||
|
||||
<p>(1) this device may not cause harmful interference, and
|
||||
<p>(1) this device may not cause harmful interference, and</p>
|
||||
<p>(2) this device must accept any interference received, including interference that may cause undesired operation.</p>
|
||||
|
||||
The following test reports are subject to this declaration:
|
||||
|
||||
@@ -307,6 +307,7 @@ class Car:
|
||||
|
||||
# sunnypilot
|
||||
self.dynamic_experimental_control = self.params.get_bool("DynamicExperimentalControl")
|
||||
self.v_cruise_helper.read_custom_set_speed_params()
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import numpy as np
|
||||
|
||||
from cereal import car
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.sunnypilot.selfdrive.car.cruise_ext import VCruiseHelperSP
|
||||
|
||||
|
||||
# WARNING: this value was determined based on the model's training distribution,
|
||||
@@ -28,8 +29,9 @@ CRUISE_INTERVAL_SIGN = {
|
||||
}
|
||||
|
||||
|
||||
class VCruiseHelper:
|
||||
class VCruiseHelper(VCruiseHelperSP):
|
||||
def __init__(self, CP):
|
||||
VCruiseHelperSP.__init__(self)
|
||||
self.CP = CP
|
||||
self.v_cruise_kph = V_CRUISE_UNSET
|
||||
self.v_cruise_cluster_kph = V_CRUISE_UNSET
|
||||
@@ -99,7 +101,7 @@ class VCruiseHelper:
|
||||
if not self.button_change_states[button_type]["enabled"]:
|
||||
return
|
||||
|
||||
v_cruise_delta = v_cruise_delta * (5 if long_press else 1)
|
||||
long_press, v_cruise_delta = VCruiseHelperSP.update_v_cruise_delta(self, long_press, v_cruise_delta)
|
||||
if long_press and self.v_cruise_kph % v_cruise_delta != 0: # partial interval
|
||||
self.v_cruise_kph = CRUISE_NEAREST_FUNC[button_type](self.v_cruise_kph / v_cruise_delta) * v_cruise_delta
|
||||
else:
|
||||
|
||||
@@ -44,7 +44,7 @@ class Controls(ControlsExt):
|
||||
|
||||
self.sm = messaging.SubMaster(['liveParameters', 'liveTorqueParameters', 'modelV2', 'selfdriveState',
|
||||
'liveCalibration', 'livePose', 'longitudinalPlan', 'carState', 'carOutput',
|
||||
'driverMonitoringState', 'onroadEvents', 'driverAssistance'] + self.sm_services_ext,
|
||||
'driverMonitoringState', 'onroadEvents', 'driverAssistance', 'liveDelay'] + self.sm_services_ext,
|
||||
poll='selfdriveState')
|
||||
self.pm = messaging.PubMaster(['carControl', 'controlsState'] + self.pm_services_ext)
|
||||
|
||||
@@ -93,6 +93,7 @@ class Controls(ControlsExt):
|
||||
torque_params.frictionCoefficientFiltered)
|
||||
|
||||
self.LaC.extension.update_model_v2(self.sm['modelV2'])
|
||||
self.LaC.extension.update_lateral_lag(self.sm['liveDelay'].lateralDelay)
|
||||
|
||||
long_plan = self.sm['longitudinalPlan']
|
||||
model_v2 = self.sm['modelV2']
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import jinja2
|
||||
import os
|
||||
|
||||
from cereal import car
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from opendbc.car.interfaces import get_interface_attr
|
||||
|
||||
Ecu = car.CarParams.Ecu
|
||||
|
||||
CARS = get_interface_attr('CAR')
|
||||
FW_VERSIONS = get_interface_attr('FW_VERSIONS')
|
||||
FINGERPRINTS = get_interface_attr('FINGERPRINTS')
|
||||
ECU_NAME = {v: k for k, v in Ecu.schema.enumerants.items()}
|
||||
|
||||
FINGERPRINTS_PY_TEMPLATE = jinja2.Template("""
|
||||
{%- if FINGERPRINTS[brand] %}
|
||||
# ruff: noqa: E501
|
||||
{% endif %}
|
||||
{% if FW_VERSIONS[brand] %}
|
||||
from opendbc.car.structs import CarParams
|
||||
{% endif %}
|
||||
from opendbc.car.{{brand}}.values import CAR
|
||||
{% if FW_VERSIONS[brand] %}
|
||||
|
||||
Ecu = CarParams.Ecu
|
||||
{% endif %}
|
||||
{% if comments +%}
|
||||
{{ comments | join() }}
|
||||
{% endif %}
|
||||
{% if FINGERPRINTS[brand] %}
|
||||
|
||||
FINGERPRINTS = {
|
||||
{% for car, fingerprints in FINGERPRINTS[brand].items() %}
|
||||
CAR.{{car.name}}: [{
|
||||
{% for fingerprint in fingerprints %}
|
||||
{% if not loop.first %}
|
||||
{{ "{" }}
|
||||
{% endif %}
|
||||
{% for key, value in fingerprint.items() %}{{key}}: {{value}}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||
|
||||
}{% if loop.last %}]{% endif %},
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
FW_VERSIONS{% if not FW_VERSIONS[brand] %}: dict[str, dict[tuple, list[bytes]]]{% endif %} = {
|
||||
{% for car, _ in FW_VERSIONS[brand].items() %}
|
||||
CAR.{{car.name}}: {
|
||||
{% for key, fw_versions in FW_VERSIONS[brand][car].items() %}
|
||||
(Ecu.{{ECU_NAME[key[0]]}}, 0x{{"%0x" | format(key[1] | int)}}, \
|
||||
{% if key[2] %}0x{{"%0x" | format(key[2] | int)}}{% else %}{{key[2]}}{% endif %}): [
|
||||
{% for fw_version in (fw_versions + extra_fw_versions.get(car, {}).get(key, [])) | unique | sort %}
|
||||
{{fw_version}},
|
||||
{% endfor %}
|
||||
],
|
||||
{% endfor %}
|
||||
},
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
""", trim_blocks=True)
|
||||
|
||||
|
||||
def format_brand_fw_versions(brand, extra_fw_versions: None | dict[str, dict[tuple, list[bytes]]] = None):
|
||||
extra_fw_versions = extra_fw_versions or {}
|
||||
|
||||
fingerprints_file = os.path.join(BASEDIR, f"opendbc/car/{brand}/fingerprints.py")
|
||||
with open(fingerprints_file) as f:
|
||||
comments = [line for line in f.readlines() if line.startswith("#") and "noqa" not in line]
|
||||
|
||||
with open(fingerprints_file, "w") as f:
|
||||
f.write(FINGERPRINTS_PY_TEMPLATE.render(brand=brand, comments=comments, ECU_NAME=ECU_NAME,
|
||||
FINGERPRINTS=FINGERPRINTS, FW_VERSIONS=FW_VERSIONS,
|
||||
extra_fw_versions=extra_fw_versions))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for brand in FW_VERSIONS.keys():
|
||||
format_brand_fw_versions(brand)
|
||||
@@ -13,6 +13,7 @@ from typing import NoReturn
|
||||
|
||||
from cereal import log, car
|
||||
import cereal.messaging as messaging
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import config_realtime_process
|
||||
@@ -36,8 +37,11 @@ RPY_INIT = np.array([0.0,0.0,0.0])
|
||||
WIDE_FROM_DEVICE_EULER_INIT = np.array([0.0, 0.0, 0.0])
|
||||
HEIGHT_INIT = np.array([1.22])
|
||||
|
||||
# These values are needed to accommodate the model frame in the narrow cam of the C3
|
||||
PITCH_LIMITS = np.array([-0.09074112085129739, 0.17])
|
||||
# These values are needed to accommodate the model frame in the narrow cam
|
||||
if HARDWARE.get_device_type() == 'mici':
|
||||
PITCH_LIMITS = np.array([-0.143101, 0.22235988])
|
||||
else:
|
||||
PITCH_LIMITS = np.array([-0.09074112085129739, 0.17])
|
||||
YAW_LIMITS = np.array([-0.06912048084718224, 0.06912048084718235])
|
||||
DEBUG = os.getenv("DEBUG") is not None
|
||||
|
||||
|
||||
@@ -82,6 +82,12 @@ class PointBuckets:
|
||||
total_points_valid = self.__len__() >= self.min_points_total
|
||||
return individual_buckets_valid and total_points_valid
|
||||
|
||||
def get_valid_percent(self) -> int:
|
||||
total_points_perc = min(self.__len__() / self.min_points_total * 100, 100)
|
||||
individual_buckets_perc = min(min(len(v) / min_pts * 100 for v, min_pts in
|
||||
zip(self.buckets.values(), self.buckets_min_points.values(), strict=True)), 100)
|
||||
return int((total_points_perc + individual_buckets_perc) / 2)
|
||||
|
||||
def is_calculable(self) -> bool:
|
||||
return all(len(v) > 0 for v in self.buckets.values())
|
||||
|
||||
|
||||
@@ -229,6 +229,8 @@ class LateralLagEstimator:
|
||||
liveDelay.lateralDelayEstimateStd = 0.0
|
||||
|
||||
liveDelay.validBlocks = self.block_avg.valid_blocks
|
||||
liveDelay.calPerc = min(100 * (self.block_avg.valid_blocks * self.block_size + self.block_avg.idx) //
|
||||
(self.min_valid_block_count * self.block_size), 100)
|
||||
if debug:
|
||||
liveDelay.points = self.block_avg.values.flatten().tolist()
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ class TestLagd:
|
||||
assert np.allclose(msg.liveDelay.lateralDelay, estimator.initial_lag)
|
||||
assert np.allclose(msg.liveDelay.lateralDelayEstimate, estimator.initial_lag)
|
||||
assert msg.liveDelay.validBlocks == 0
|
||||
assert msg.liveDelay.calPerc == 0
|
||||
|
||||
def test_estimator_basics(self, subtests):
|
||||
for lag_frames in range(5):
|
||||
@@ -107,6 +108,7 @@ class TestLagd:
|
||||
assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
|
||||
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
|
||||
assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED
|
||||
assert msg.liveDelay.calPerc == 100
|
||||
|
||||
def test_disabled_estimator(self):
|
||||
mocked_CP = car.CarParams(steerActuatorDelay=0.8)
|
||||
@@ -119,6 +121,7 @@ class TestLagd:
|
||||
assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
|
||||
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
|
||||
assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED
|
||||
assert msg.liveDelay.calPerc == 100
|
||||
|
||||
def test_estimator_masking(self):
|
||||
mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.8), random.randint(1, 19)
|
||||
@@ -127,6 +130,7 @@ class TestLagd:
|
||||
msg = estimator.get_msg(True)
|
||||
assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
|
||||
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
|
||||
assert msg.liveDelay.calPerc == 100
|
||||
|
||||
@pytest.mark.skipif(PC, reason="only on device")
|
||||
@pytest.mark.timeout(60)
|
||||
|
||||
25
selfdrive/locationd/test/test_torqued.py
Normal file
25
selfdrive/locationd/test/test_torqued.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from cereal import car
|
||||
from openpilot.selfdrive.locationd.torqued import TorqueEstimator
|
||||
|
||||
|
||||
def test_cal_percent():
|
||||
est = TorqueEstimator(car.CarParams())
|
||||
msg = est.get_msg()
|
||||
assert msg.liveTorqueParameters.calPerc == 0
|
||||
|
||||
for (low, high), min_pts in zip(est.filtered_points.buckets.keys(),
|
||||
est.filtered_points.buckets_min_points.values(), strict=True):
|
||||
for _ in range(int(min_pts)):
|
||||
est.filtered_points.add_point((low + high) / 2.0, 0.0)
|
||||
|
||||
# enough bucket points, but not enough total points
|
||||
msg = est.get_msg()
|
||||
assert msg.liveTorqueParameters.calPerc == (len(est.filtered_points) / est.min_points_total * 100 + 100) / 2
|
||||
|
||||
# add enough points to bucket with most capacity
|
||||
key = list(est.filtered_points.buckets)[0]
|
||||
for _ in range(est.min_points_total - len(est.filtered_points)):
|
||||
est.filtered_points.add_point((key[0] + key[1]) / 2.0, 0.0)
|
||||
|
||||
msg = est.get_msg()
|
||||
assert msg.liveTorqueParameters.calPerc == 100
|
||||
@@ -233,6 +233,7 @@ class TorqueEstimator(ParameterEstimator):
|
||||
liveTorqueParameters.latAccelOffsetFiltered = float(self.filtered_params['latAccelOffset'].x)
|
||||
liveTorqueParameters.frictionCoefficientFiltered = float(self.filtered_params['frictionCoefficient'].x)
|
||||
liveTorqueParameters.totalBucketPoints = len(self.filtered_points)
|
||||
liveTorqueParameters.calPerc = self.filtered_points.get_valid_percent()
|
||||
liveTorqueParameters.decay = self.decay
|
||||
liveTorqueParameters.maxResets = self.resets
|
||||
return msg
|
||||
|
||||
@@ -60,7 +60,7 @@ import subprocess
|
||||
from tinygrad import Device
|
||||
|
||||
# because tg doesn't support multi-process
|
||||
devs = subprocess.check_output('python3 -c "from tinygrad import Device; print(list(Device.get_available_devices()))"', shell=True)
|
||||
devs = subprocess.check_output('python3 -c "from tinygrad import Device; print(list(Device.get_available_devices()))"', shell=True, cwd=env.Dir('#').abspath)
|
||||
if b"AMD" in devs:
|
||||
del Device
|
||||
print("USB GPU detected... building")
|
||||
|
||||
@@ -86,10 +86,20 @@ class ModelState:
|
||||
prev_desire: np.ndarray # for tracking the rising edge of the pulse
|
||||
|
||||
def __init__(self, context: CLContext):
|
||||
self.frames = {
|
||||
'input_imgs': DrivingModelFrame(context, ModelConstants.TEMPORAL_SKIP),
|
||||
'big_input_imgs': DrivingModelFrame(context, ModelConstants.TEMPORAL_SKIP)
|
||||
}
|
||||
with open(VISION_METADATA_PATH, 'rb') as f:
|
||||
vision_metadata = pickle.load(f)
|
||||
self.vision_input_shapes = vision_metadata['input_shapes']
|
||||
self.vision_input_names = list(self.vision_input_shapes.keys())
|
||||
self.vision_output_slices = vision_metadata['output_slices']
|
||||
vision_output_size = vision_metadata['output_shapes']['outputs'][1]
|
||||
|
||||
with open(POLICY_METADATA_PATH, 'rb') as f:
|
||||
policy_metadata = pickle.load(f)
|
||||
self.policy_input_shapes = policy_metadata['input_shapes']
|
||||
self.policy_output_slices = policy_metadata['output_slices']
|
||||
policy_output_size = policy_metadata['output_shapes']['outputs'][1]
|
||||
|
||||
self.frames = {name: DrivingModelFrame(context, ModelConstants.TEMPORAL_SKIP) for name in self.vision_input_names}
|
||||
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
|
||||
|
||||
self.full_features_buffer = np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32)
|
||||
@@ -106,18 +116,6 @@ class ModelState:
|
||||
'features_buffer': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32),
|
||||
}
|
||||
|
||||
with open(VISION_METADATA_PATH, 'rb') as f:
|
||||
vision_metadata = pickle.load(f)
|
||||
self.vision_input_shapes = vision_metadata['input_shapes']
|
||||
self.vision_output_slices = vision_metadata['output_slices']
|
||||
vision_output_size = vision_metadata['output_shapes']['outputs'][1]
|
||||
|
||||
with open(POLICY_METADATA_PATH, 'rb') as f:
|
||||
policy_metadata = pickle.load(f)
|
||||
self.policy_input_shapes = policy_metadata['input_shapes']
|
||||
self.policy_output_slices = policy_metadata['output_slices']
|
||||
policy_output_size = policy_metadata['output_shapes']['outputs'][1]
|
||||
|
||||
# img buffers are managed in openCL transform code
|
||||
self.vision_inputs: dict[str, Tensor] = {}
|
||||
self.vision_output = np.zeros(vision_output_size, dtype=np.float32)
|
||||
@@ -135,7 +133,7 @@ class ModelState:
|
||||
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
|
||||
return parsed_model_outputs
|
||||
|
||||
def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray,
|
||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
||||
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
|
||||
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
||||
inputs['desire'][0] = 0
|
||||
@@ -148,8 +146,7 @@ class ModelState:
|
||||
|
||||
self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention']
|
||||
self.numpy_inputs['lateral_control_params'][:] = inputs['lateral_control_params']
|
||||
imgs_cl = {'input_imgs': self.frames['input_imgs'].prepare(buf, transform.flatten()),
|
||||
'big_input_imgs': self.frames['big_input_imgs'].prepare(wbuf, transform_wide.flatten())}
|
||||
imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.vision_input_names}
|
||||
|
||||
if TICI and not USBGPU:
|
||||
# The imgs tensors are backed by opencl memory, only need init once
|
||||
@@ -328,14 +325,16 @@ def main(demo=False):
|
||||
if prepare_only:
|
||||
cloudlog.error(f"skipping model eval. Dropped {vipc_dropped_frames} frames")
|
||||
|
||||
bufs = {name: buf_extra if 'big' in name else buf_main for name in model.vision_input_names}
|
||||
transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.vision_input_names}
|
||||
inputs:dict[str, np.ndarray] = {
|
||||
'desire': vec_desire,
|
||||
'traffic_convention': traffic_convention,
|
||||
'lateral_control_params': lateral_control_params,
|
||||
}
|
||||
}
|
||||
|
||||
mt1 = time.perf_counter()
|
||||
model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only)
|
||||
model_output = model.run(bufs, transforms, inputs, prepare_only)
|
||||
mt2 = time.perf_counter()
|
||||
model_execution_time = mt2 - mt1
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -585,11 +585,6 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
},
|
||||
|
||||
EventName.noGps: {
|
||||
ET.PERMANENT: Alert(
|
||||
"Poor GPS reception",
|
||||
"Ensure device has a clear view of the sky",
|
||||
AlertStatus.normal, AlertSize.mid,
|
||||
Priority.LOWER, VisualAlert.none, AudibleAlert.none, .2, creation_delay=600.)
|
||||
},
|
||||
|
||||
EventName.tooDistracted: {
|
||||
|
||||
@@ -382,16 +382,16 @@ class SelfdriveD(CruiseHelper):
|
||||
if (planner_fcw or model_fcw) and not self.CP.notCar:
|
||||
self.events.add(EventName.fcw)
|
||||
|
||||
# GPS checks
|
||||
gps_ok = self.sm.recv_frame[self.gps_location_service] > 0 and (self.sm.frame - self.sm.recv_frame[self.gps_location_service]) * DT_CTRL < 2.0
|
||||
if not gps_ok and self.sm['livePose'].inputsOK and (self.distance_traveled > 1500):
|
||||
self.events.add(EventName.noGps)
|
||||
if gps_ok:
|
||||
self.distance_traveled = 0
|
||||
self.distance_traveled += abs(CS.vEgo) * DT_CTRL
|
||||
|
||||
# TODO: fix simulator
|
||||
if not SIMULATION or REPLAY:
|
||||
# Not show in first 1.5 km to allow for driving out of garage. This event shows after 5 minutes
|
||||
gps_ok = self.sm.recv_frame[self.gps_location_service] > 0 and (self.sm.frame - self.sm.recv_frame[self.gps_location_service]) * DT_CTRL < 2.0
|
||||
if not gps_ok and self.sm['livePose'].inputsOK and (self.distance_traveled > 1500):
|
||||
self.events.add(EventName.noGps)
|
||||
if gps_ok:
|
||||
self.distance_traveled = 0
|
||||
self.distance_traveled += abs(CS.vEgo) * DT_CTRL
|
||||
|
||||
if self.sm['modelV2'].frameDropPerc > 20:
|
||||
self.events.add(EventName.modeldLagging)
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
|
||||
OP_ROOT="$DIR/../../"
|
||||
|
||||
if [ -z "$BUILD" ]; then
|
||||
docker pull ghcr.io/commaai/openpilot-base:latest
|
||||
else
|
||||
docker build --cache-from ghcr.io/commaai/openpilot-base:latest -t ghcr.io/commaai/openpilot-base:latest -f $OP_ROOT/Dockerfile.openpilot_base .
|
||||
fi
|
||||
|
||||
docker run \
|
||||
-it \
|
||||
--rm \
|
||||
--volume $OP_ROOT:$OP_ROOT \
|
||||
--workdir $PWD \
|
||||
--env PYTHONPATH=$OP_ROOT \
|
||||
ghcr.io/commaai/openpilot-base:latest \
|
||||
/bin/bash
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from typing import Any
|
||||
@@ -189,22 +190,44 @@ def model_replay(lr, frs):
|
||||
print("----------------- Model Timing -----------------")
|
||||
print("------------------------------------------------")
|
||||
print(tabulate(rows, header, tablefmt="simple_grid", stralign="center", numalign="center", floatfmt=".4f"))
|
||||
assert timings_ok
|
||||
assert timings_ok or PC
|
||||
|
||||
return msgs
|
||||
|
||||
|
||||
def get_frames():
|
||||
regen_cache = "--regen-cache" in sys.argv
|
||||
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__":
|
||||
update = "--update" in sys.argv or (os.getenv("GIT_BRANCH", "") == 'master')
|
||||
replay_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# load logs
|
||||
lr = list(LogReader(get_url(TEST_ROUTE, SEGMENT, "rlog.zst")))
|
||||
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)
|
||||
}
|
||||
frs = get_frames()
|
||||
|
||||
log_msgs = []
|
||||
# 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.capture import ProcessOutputCapture
|
||||
from openpilot.tools.lib.logreader import LogIterable
|
||||
from openpilot.tools.lib.framereader import BaseFrameReader
|
||||
from openpilot.tools.lib.framereader import FrameReader
|
||||
|
||||
# Numpy gives different results based on CPU features after version 19
|
||||
NUMPY_TOLERANCE = 1e-7
|
||||
@@ -209,6 +209,7 @@ class ProcessContainer:
|
||||
streams_metas = available_streams(all_msgs)
|
||||
for meta in streams_metas:
|
||||
if meta.camera_state in self.cfg.vision_pubs:
|
||||
assert frs[meta.camera_state].pix_fmt == 'nv12'
|
||||
frame_size = (frs[meta.camera_state].w, frs[meta.camera_state].h)
|
||||
vipc_server.create_buffers(meta.stream, 2, *frame_size)
|
||||
vipc_server.start_listener()
|
||||
@@ -224,7 +225,7 @@ class ProcessContainer:
|
||||
|
||||
def start(
|
||||
self, params_config: dict[str, Any], environ_config: dict[str, Any],
|
||||
all_msgs: LogIterable, frs: dict[str, BaseFrameReader] | None,
|
||||
all_msgs: LogIterable, frs: dict[str, FrameReader] | None,
|
||||
fingerprint: str | None, capture_output: bool
|
||||
):
|
||||
with self.prefix as p:
|
||||
@@ -266,7 +267,7 @@ class ProcessContainer:
|
||||
self.prefix.clean_dirs()
|
||||
self._clean_env()
|
||||
|
||||
def run_step(self, msg: capnp._DynamicStructReader, frs: dict[str, BaseFrameReader] | None) -> list[capnp._DynamicStructReader]:
|
||||
def run_step(self, msg: capnp._DynamicStructReader, frs: dict[str, FrameReader] | None) -> list[capnp._DynamicStructReader]:
|
||||
assert self.rc and self.pm and self.sockets and self.process.proc
|
||||
|
||||
output_msgs = []
|
||||
@@ -296,7 +297,7 @@ class ProcessContainer:
|
||||
camera_state = getattr(m, m.which())
|
||||
camera_meta = meta_from_camera_state(m.which())
|
||||
assert frs is not None
|
||||
img = frs[m.which()].get(camera_state.frameId, pix_fmt="nv12")[0]
|
||||
img = frs[m.which()].get(camera_state.frameId)
|
||||
self.vipc_server.send(camera_meta.stream, img.flatten().tobytes(),
|
||||
camera_state.frameId, camera_state.timestampSof, camera_state.timestampEof)
|
||||
self.msg_queue = []
|
||||
@@ -655,7 +656,7 @@ def replay_process_with_name(name: str | Iterable[str], lr: LogIterable, *args,
|
||||
|
||||
|
||||
def replay_process(
|
||||
cfg: ProcessConfig | Iterable[ProcessConfig], lr: LogIterable, frs: dict[str, BaseFrameReader] = None,
|
||||
cfg: ProcessConfig | Iterable[ProcessConfig], lr: LogIterable, frs: dict[str, FrameReader] = None,
|
||||
fingerprint: str = None, return_all_logs: bool = False, custom_params: dict[str, Any] = None,
|
||||
captured_output_store: dict[str, dict[str, str]] = None, disable_progress: bool = False
|
||||
) -> list[capnp._DynamicStructReader]:
|
||||
@@ -683,7 +684,7 @@ def replay_process(
|
||||
|
||||
|
||||
def _replay_multi_process(
|
||||
cfgs: list[ProcessConfig], lr: LogIterable, frs: dict[str, BaseFrameReader] | None, fingerprint: str | None,
|
||||
cfgs: list[ProcessConfig], lr: LogIterable, frs: dict[str, FrameReader] | None, fingerprint: str | None,
|
||||
custom_params: dict[str, Any] | None, captured_output_store: dict[str, dict[str, str]] | None, disable_progress: bool
|
||||
) -> list[capnp._DynamicStructReader]:
|
||||
if fingerprint is not None:
|
||||
|
||||
@@ -1 +1 @@
|
||||
9e2fe2942fbf77f24bccdbef15893831f9c0b390
|
||||
f440c9e0469d32d350aa99ddaa8f44591a2ce690
|
||||
@@ -3,40 +3,17 @@ import os
|
||||
import argparse
|
||||
import time
|
||||
import capnp
|
||||
import numpy as np
|
||||
|
||||
from typing import Any
|
||||
from collections.abc import Iterable
|
||||
|
||||
from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, FAKEDATA, ProcessConfig, replay_process, get_process_config, \
|
||||
check_openpilot_enabled, check_most_messages_valid, get_custom_params_from_lr
|
||||
from openpilot.selfdrive.test.process_replay.vision_meta import DRIVER_CAMERA_FRAME_SIZES
|
||||
from openpilot.selfdrive.test.update_ci_routes import upload_route
|
||||
from openpilot.tools.lib.framereader import FrameReader, BaseFrameReader, FrameType
|
||||
from openpilot.tools.lib.framereader import FrameReader
|
||||
from openpilot.tools.lib.logreader import LogReader, LogIterable, save_log
|
||||
from openpilot.tools.lib.openpilotci import get_url
|
||||
|
||||
class DummyFrameReader(BaseFrameReader):
|
||||
def __init__(self, w: int, h: int, frame_count: int, pix_val: int):
|
||||
self.pix_val = pix_val
|
||||
self.w, self.h = w, h
|
||||
self.frame_count = frame_count
|
||||
self.frame_type = FrameType.raw
|
||||
|
||||
def get(self, idx, count=1, pix_fmt="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(
|
||||
lr: LogIterable, frs: dict[str, Any] = None,
|
||||
@@ -64,7 +41,7 @@ def setup_data_readers(
|
||||
frs['wideRoadCameraState'] = FrameReader(get_url(route, str(sidx), "ecamera.hevc"))
|
||||
if needs_driver_cam:
|
||||
if dummy_driver_cam:
|
||||
frs['driverCameraState'] = DummyFrameReader.zero_dcamera()
|
||||
frs['driverCameraState'] = FrameReader(get_url(route, str(sidx), "fcamera.hevc")) # Use fcam as dummy
|
||||
else:
|
||||
device_type = next(str(msg.initData.deviceType) for msg in lr if msg.which() == "initData")
|
||||
assert device_type != "neo", "Driver camera not supported on neo segments. Use dummy dcamera."
|
||||
|
||||
@@ -19,7 +19,6 @@ from openpilot.tools.lib.logreader import LogReader, save_log
|
||||
IS_AZURE_TOKEN_DEFINED = os.getenv("AZURE_TOKEN")
|
||||
|
||||
source_segments = [
|
||||
("BODY", "937ccb7243511b65|2022-05-24--16-03-09--1"), # COMMA.COMMA_BODY
|
||||
("HYUNDAI", "02c45f73a2e5c6e9|2021-01-01--19-08-22--1"), # HYUNDAI.HYUNDAI_SONATA
|
||||
("HYUNDAI2", "d545129f3ca90f28|2022-11-07--20-43-08--3"), # HYUNDAI.HYUNDAI_KIA_EV6 (+ QCOM GPS)
|
||||
("TOYOTA", "0982d79ebb0de295|2021-01-04--17-13-21--13"), # TOYOTA.TOYOTA_PRIUS
|
||||
@@ -44,7 +43,6 @@ source_segments = [
|
||||
]
|
||||
|
||||
segments = [
|
||||
("BODY", "regen2F3C7259F1B|2025-04-08--23-00-23--0"),
|
||||
("HYUNDAI", "regenAA0FC4ED71E|2025-04-08--22-57-50--0"),
|
||||
("HYUNDAI2", "regenAFB9780D823|2025-04-08--23-00-34--0"),
|
||||
("TOYOTA", "regen218A4DCFAA1|2025-04-08--22-57-51--0"),
|
||||
@@ -65,7 +63,7 @@ segments = [
|
||||
]
|
||||
|
||||
# dashcamOnly makes don't need to be tested until a full port is done
|
||||
excluded_interfaces = ["mock", "tesla"]
|
||||
excluded_interfaces = ["mock", "body"]
|
||||
|
||||
BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/"
|
||||
REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit")
|
||||
@@ -253,7 +251,7 @@ if __name__ == "__main__":
|
||||
continue
|
||||
|
||||
# to speed things up, we only test all segments on card
|
||||
if cfg.proc_name != 'card' and car_brand not in ('HYUNDAI', 'TOYOTA', 'HONDA', 'SUBARU', 'FORD', 'RIVIAN', 'TESLA'):
|
||||
if cfg.proc_name not in ('card', 'controlsd', 'lagd') and car_brand not in ('HYUNDAI', 'TOYOTA'):
|
||||
continue
|
||||
|
||||
cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from parameterized import parameterized
|
||||
|
||||
from openpilot.selfdrive.test.process_replay.regen import regen_segment, DummyFrameReader
|
||||
from openpilot.selfdrive.test.process_replay.regen import regen_segment
|
||||
from openpilot.selfdrive.test.process_replay.process_replay import check_openpilot_enabled
|
||||
from openpilot.tools.lib.openpilotci import get_url
|
||||
from openpilot.tools.lib.logreader import LogReader
|
||||
@@ -18,7 +18,7 @@ def ci_setup_data_readers(route, sidx):
|
||||
lr = LogReader(get_url(route, sidx, "rlog.bz2"))
|
||||
frs = {
|
||||
'roadCameraState': FrameReader(get_url(route, sidx, "fcamera.hevc")),
|
||||
'driverCameraState': DummyFrameReader.zero_dcamera()
|
||||
'driverCameraState': FrameReader(get_url(route, sidx, "fcamera.hevc")),
|
||||
}
|
||||
if next((True for m in lr if m.which() == "wideRoadCameraState"), False):
|
||||
frs["wideRoadCameraState"] = FrameReader(get_url(route, sidx, "ecamera.hevc"))
|
||||
|
||||
@@ -112,7 +112,7 @@ if GetOption('extras'):
|
||||
obj = raylib_env.Object(f"installer/installers/installer_{name}.o", ["installer/installer.cc"], CPPDEFINES=d)
|
||||
f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter], LIBS=raylib_libs)
|
||||
# keep installers small
|
||||
assert f[0].get_size() < 1300*1e3, f[0].get_size()
|
||||
assert f[0].get_size() < 1900*1e3, f[0].get_size()
|
||||
|
||||
# build watch3
|
||||
if arch in ['x86_64', 'aarch64', 'Darwin'] or GetOption('extras'):
|
||||
|
||||
@@ -4,10 +4,12 @@ from collections.abc import Callable
|
||||
from enum import IntEnum
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.widgets.offroad_alerts import UpdateAlert, OffroadAlert
|
||||
from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButton
|
||||
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
|
||||
from openpilot.selfdrive.ui.widgets.setup import SetupWidget
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.label import gui_label
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR
|
||||
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
|
||||
HEADER_HEIGHT = 80
|
||||
HEAD_BUTTON_FONT_SIZE = 40
|
||||
@@ -25,8 +27,9 @@ class HomeLayoutState(IntEnum):
|
||||
ALERTS = 2
|
||||
|
||||
|
||||
class HomeLayout:
|
||||
class HomeLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.params = Params()
|
||||
|
||||
self.update_alert = UpdateAlert()
|
||||
@@ -47,6 +50,10 @@ class HomeLayout:
|
||||
self.update_notif_rect = rl.Rectangle(0, 0, 200, HEADER_HEIGHT - 10)
|
||||
self.alert_notif_rect = rl.Rectangle(0, 0, 220, HEADER_HEIGHT - 10)
|
||||
|
||||
self._prime_widget = PrimeWidget()
|
||||
self._setup_widget = SetupWidget()
|
||||
|
||||
self._exp_mode_button = ExperimentalModeButton()
|
||||
self._setup_callbacks()
|
||||
|
||||
def _setup_callbacks(self):
|
||||
@@ -59,9 +66,7 @@ class HomeLayout:
|
||||
def _set_state(self, state: HomeLayoutState):
|
||||
self.current_state = state
|
||||
|
||||
def render(self, rect: rl.Rectangle):
|
||||
self._update_layout_rects(rect)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
current_time = time.time()
|
||||
if current_time - self.last_refresh >= REFRESH_INTERVAL:
|
||||
self._refresh()
|
||||
@@ -78,16 +83,16 @@ class HomeLayout:
|
||||
elif self.current_state == HomeLayoutState.ALERTS:
|
||||
self._render_alerts_view()
|
||||
|
||||
def _update_layout_rects(self, rect: rl.Rectangle):
|
||||
def _update_layout_rects(self):
|
||||
self.header_rect = rl.Rectangle(
|
||||
rect.x + CONTENT_MARGIN, rect.y + CONTENT_MARGIN, rect.width - 2 * CONTENT_MARGIN, HEADER_HEIGHT
|
||||
self._rect.x + CONTENT_MARGIN, self._rect.y + CONTENT_MARGIN, self._rect.width - 2 * CONTENT_MARGIN, HEADER_HEIGHT
|
||||
)
|
||||
|
||||
content_y = rect.y + CONTENT_MARGIN + HEADER_HEIGHT + SPACING
|
||||
content_height = rect.height - CONTENT_MARGIN - HEADER_HEIGHT - SPACING - CONTENT_MARGIN
|
||||
content_y = self._rect.y + CONTENT_MARGIN + HEADER_HEIGHT + SPACING
|
||||
content_height = self._rect.height - CONTENT_MARGIN - HEADER_HEIGHT - SPACING - CONTENT_MARGIN
|
||||
|
||||
self.content_rect = rl.Rectangle(
|
||||
rect.x + CONTENT_MARGIN, content_y, rect.width - 2 * CONTENT_MARGIN, content_height
|
||||
self._rect.x + CONTENT_MARGIN, content_y, self._rect.width - 2 * CONTENT_MARGIN, content_height
|
||||
)
|
||||
|
||||
left_width = self.content_rect.width - RIGHT_COLUMN_WIDTH - SPACING
|
||||
@@ -170,28 +175,25 @@ class HomeLayout:
|
||||
self.offroad_alert.render(self.content_rect)
|
||||
|
||||
def _render_left_column(self):
|
||||
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)
|
||||
self._prime_widget.render(self.left_column_rect)
|
||||
|
||||
def _render_right_column(self):
|
||||
widget_height = (self.right_column_rect.height - SPACING) // 2
|
||||
|
||||
exp_height = 125
|
||||
exp_rect = rl.Rectangle(
|
||||
self.right_column_rect.x, self.right_column_rect.y, self.right_column_rect.width, widget_height
|
||||
self.right_column_rect.x, self.right_column_rect.y, self.right_column_rect.width, exp_height
|
||||
)
|
||||
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)
|
||||
self._exp_mode_button.render(exp_rect)
|
||||
|
||||
setup_rect = rl.Rectangle(
|
||||
self.right_column_rect.x,
|
||||
self.right_column_rect.y + widget_height + SPACING,
|
||||
self.right_column_rect.y + exp_height + SPACING,
|
||||
self.right_column_rect.width,
|
||||
widget_height,
|
||||
self.right_column_rect.height - exp_height - SPACING,
|
||||
)
|
||||
rl.draw_rectangle_rounded(setup_rect, 0.02, 10, PRIME_BG_COLOR)
|
||||
gui_label(setup_rect, "Setup", 36, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
self._setup_widget.render(setup_rect)
|
||||
|
||||
def _refresh(self):
|
||||
# TODO: implement _update_state with a timer
|
||||
self.update_available = self.update_alert.refresh()
|
||||
self.alert_count = self.offroad_alert.refresh()
|
||||
self._update_state_priority(self.update_available, self.alert_count > 0)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import pyray as rl
|
||||
from enum import IntEnum
|
||||
import cereal.messaging as messaging
|
||||
from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH
|
||||
from openpilot.selfdrive.ui.layouts.home import HomeLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType
|
||||
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
||||
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
|
||||
|
||||
class MainState(IntEnum):
|
||||
@@ -13,14 +15,15 @@ class MainState(IntEnum):
|
||||
ONROAD = 2
|
||||
|
||||
|
||||
class MainLayout:
|
||||
class MainLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._pm = messaging.PubMaster(['userFlag'])
|
||||
|
||||
self._sidebar = Sidebar()
|
||||
self._sidebar_visible = True
|
||||
self._current_mode = MainState.HOME
|
||||
self._prev_onroad = False
|
||||
self._window_rect = None
|
||||
self._current_callback: callable | None = None
|
||||
|
||||
# Initialize layouts
|
||||
self._layouts = {MainState.HOME: HomeLayout(), MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()}
|
||||
@@ -31,32 +34,23 @@ class MainLayout:
|
||||
# Set callbacks
|
||||
self._setup_callbacks()
|
||||
|
||||
def render(self, rect):
|
||||
self._current_callback = None
|
||||
|
||||
self._update_layout_rects(rect)
|
||||
def _render(self, _):
|
||||
self._handle_onroad_transition()
|
||||
self._render_main_content()
|
||||
self._handle_input()
|
||||
|
||||
if self._current_callback:
|
||||
self._current_callback()
|
||||
|
||||
def _setup_callbacks(self):
|
||||
self._sidebar.set_callbacks(
|
||||
on_settings=lambda: setattr(self, '_current_callback', self._on_settings_clicked),
|
||||
on_flag=lambda: setattr(self, '_current_callback', self._on_flag_clicked),
|
||||
)
|
||||
self._layouts[MainState.SETTINGS].set_callbacks(
|
||||
on_close=lambda: setattr(self, '_current_callback', self._set_mode_for_state)
|
||||
)
|
||||
self._sidebar.set_callbacks(on_settings=self._on_settings_clicked,
|
||||
on_flag=self._on_flag_clicked)
|
||||
self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE))
|
||||
self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state)
|
||||
self._layouts[MainState.ONROAD].set_callbacks(on_click=self._on_onroad_clicked)
|
||||
device.add_interactive_timeout_callback(self._set_mode_for_state)
|
||||
|
||||
def _update_layout_rects(self, rect):
|
||||
self._window_rect = rect
|
||||
self._sidebar_rect = rl.Rectangle(rect.x, rect.y, SIDEBAR_WIDTH, rect.height)
|
||||
def _update_layout_rects(self):
|
||||
self._sidebar_rect = rl.Rectangle(self._rect.x, self._rect.y, SIDEBAR_WIDTH, self._rect.height)
|
||||
|
||||
x_offset = SIDEBAR_WIDTH if self._sidebar_visible else 0
|
||||
self._content_rect = rl.Rectangle(rect.y + x_offset, rect.y, rect.width - x_offset, rect.height)
|
||||
x_offset = SIDEBAR_WIDTH if self._sidebar.is_visible else 0
|
||||
self._content_rect = rl.Rectangle(self._rect.y + x_offset, self._rect.y, self._rect.width - x_offset, self._rect.height)
|
||||
|
||||
def _handle_onroad_transition(self):
|
||||
if ui_state.started != self._prev_onroad:
|
||||
@@ -66,31 +60,34 @@ class MainLayout:
|
||||
|
||||
def _set_mode_for_state(self):
|
||||
if ui_state.started:
|
||||
# Don't hide sidebar from interactive timeout
|
||||
if self._current_mode != MainState.ONROAD:
|
||||
self._sidebar.set_visible(False)
|
||||
self._current_mode = MainState.ONROAD
|
||||
self._sidebar_visible = False
|
||||
else:
|
||||
self._current_mode = MainState.HOME
|
||||
self._sidebar_visible = True
|
||||
self._sidebar.set_visible(True)
|
||||
|
||||
def open_settings(self, panel_type: PanelType):
|
||||
self._layouts[MainState.SETTINGS].set_current_panel(panel_type)
|
||||
self._current_mode = MainState.SETTINGS
|
||||
self._sidebar.set_visible(False)
|
||||
|
||||
def _on_settings_clicked(self):
|
||||
self._current_mode = MainState.SETTINGS
|
||||
self._sidebar_visible = False
|
||||
self.open_settings(PanelType.DEVICE)
|
||||
|
||||
def _on_flag_clicked(self):
|
||||
pass
|
||||
user_flag = messaging.new_message('userFlag')
|
||||
user_flag.valid = True
|
||||
self._pm.send('userFlag', user_flag)
|
||||
|
||||
def _on_onroad_clicked(self):
|
||||
self._sidebar.set_visible(not self._sidebar.is_visible)
|
||||
|
||||
def _render_main_content(self):
|
||||
# Render sidebar
|
||||
if self._sidebar_visible:
|
||||
if self._sidebar.is_visible:
|
||||
self._sidebar.render(self._sidebar_rect)
|
||||
|
||||
content_rect = self._content_rect if self._sidebar_visible else self._window_rect
|
||||
content_rect = self._content_rect if self._sidebar.is_visible else self._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,19 +1,17 @@
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper
|
||||
from openpilot.system.ui.widgets.network import WifiManagerUI
|
||||
|
||||
|
||||
class NetworkLayout:
|
||||
class NetworkLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.wifi_manager = WifiManagerWrapper()
|
||||
self.wifi_ui = WifiManagerUI(self.wifi_manager)
|
||||
|
||||
def render(self, rect: rl.Rectangle):
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self.wifi_ui.render(rect)
|
||||
|
||||
@property
|
||||
def require_full_screen(self):
|
||||
return self.wifi_ui.require_full_screen
|
||||
|
||||
def shutdown(self):
|
||||
self.wifi_manager.shutdown()
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from openpilot.system.ui.lib.list_view import ListView, toggle_item
|
||||
from openpilot.system.ui.lib.list_view import toggle_item
|
||||
from openpilot.system.ui.lib.scroller import Scroller
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
@@ -8,11 +11,16 @@ DESCRIPTIONS = {
|
||||
"See https://docs.comma.ai/how-to/connect-to-comma for more info."
|
||||
),
|
||||
'joystick_debug_mode': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
|
||||
'ssh_key': (
|
||||
"Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username " +
|
||||
"other than your own. A comma employee will NEVER ask you to add their GitHub username."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class DeveloperLayout:
|
||||
class DeveloperLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
items = [
|
||||
toggle_item(
|
||||
@@ -21,6 +29,7 @@ class DeveloperLayout:
|
||||
initial_state=self._params.get_bool("AdbEnabled"),
|
||||
callback=self._on_enable_adb,
|
||||
),
|
||||
ssh_key_item("SSH Key", description=DESCRIPTIONS["ssh_key"]),
|
||||
toggle_item(
|
||||
"Joystick Debug Mode",
|
||||
description=DESCRIPTIONS["joystick_debug_mode"],
|
||||
@@ -41,10 +50,10 @@ class DeveloperLayout:
|
||||
),
|
||||
]
|
||||
|
||||
self._list_widget = ListView(items)
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def render(self, rect):
|
||||
self._list_widget.render(rect)
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def _on_enable_adb(self): pass
|
||||
def _on_joystick_debug_mode(self): pass
|
||||
|
||||
@@ -1,47 +1,150 @@
|
||||
from openpilot.system.ui.lib.list_view import ListView, text_item, button_item
|
||||
import os
|
||||
import json
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.hardware import TICI
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.list_view import text_item, button_item, dual_button_item
|
||||
from openpilot.system.ui.lib.scroller import Scroller
|
||||
from openpilot.system.ui.lib.widget import Widget, DialogResult
|
||||
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog, alert_dialog
|
||||
from openpilot.system.ui.widgets.html_render import HtmlRenderer
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
'pair_device': "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.",
|
||||
'driver_camera': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
|
||||
'reset_calibration': (
|
||||
"openpilot requires the device to be mounted within 4° left or right and within 5° " +
|
||||
"up or 9° down. openpilot is continuously calibrating, resetting is rarely required."
|
||||
"openpilot requires the device to be mounted within 4° left or right and within 5° " +
|
||||
"up or 9° down. openpilot is continuously calibrating, resetting is rarely required."
|
||||
),
|
||||
'review_guide': "Review the rules, features, and limitations of openpilot",
|
||||
}
|
||||
|
||||
|
||||
class DeviceLayout:
|
||||
class DeviceLayout(Widget):
|
||||
def __init__(self):
|
||||
params = Params()
|
||||
dongle_id = params.get("DongleId", encoding="utf-8") or "N/A"
|
||||
serial = params.get("HardwareSerial") or "N/A"
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
self._select_language_dialog: MultiOptionDialog | None = None
|
||||
self._driver_camera: DriverCameraDialog | None = None
|
||||
self._pair_device_dialog: PairingDialog | None = None
|
||||
self._fcc_dialog: HtmlRenderer | None = None
|
||||
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
dongle_id = self._params.get("DongleId", encoding="utf-8") or "N/A"
|
||||
serial = self._params.get("HardwareSerial") or "N/A"
|
||||
|
||||
items = [
|
||||
text_item("Dongle ID", dongle_id),
|
||||
text_item("Serial", serial),
|
||||
button_item("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], self._on_pair_device),
|
||||
button_item("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], self._on_driver_camera),
|
||||
button_item("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], self._on_reset_calibration),
|
||||
button_item("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], callback=self._pair_device),
|
||||
button_item("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], callback=self._show_driver_camera, enabled=ui_state.is_offroad),
|
||||
button_item("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt),
|
||||
regulatory_btn := button_item("Regulatory", "VIEW", callback=self._on_regulatory),
|
||||
button_item("Review Training Guide", "REVIEW", DESCRIPTIONS['review_guide'], self._on_review_training_guide),
|
||||
button_item("Change Language", "CHANGE", callback=self._show_language_selection, enabled=ui_state.is_offroad),
|
||||
dual_button_item("Reboot", "Power Off", left_callback=self._reboot_prompt, right_callback=self._power_off_prompt),
|
||||
]
|
||||
regulatory_btn.set_visible(TICI)
|
||||
return items
|
||||
|
||||
if TICI:
|
||||
items.append(button_item("Regulatory", "VIEW", callback=self._on_regulatory))
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
items.append(button_item("Change Language", "CHANGE", callback=self._on_change_language))
|
||||
def _show_language_selection(self):
|
||||
try:
|
||||
languages_file = os.path.join(BASEDIR, "selfdrive/ui/translations/languages.json")
|
||||
with open(languages_file, encoding='utf-8') as f:
|
||||
languages = json.load(f)
|
||||
|
||||
self._list_widget = ListView(items)
|
||||
self._select_language_dialog = MultiOptionDialog("Select a language", languages)
|
||||
gui_app.set_modal_overlay(self._select_language_dialog, callback=self._handle_language_selection)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def render(self, rect):
|
||||
self._list_widget.render(rect)
|
||||
def _handle_language_selection(self, result: int):
|
||||
if result == 1 and self._select_language_dialog:
|
||||
selected_language = self._select_language_dialog.selection
|
||||
self._params.put("LanguageSetting", selected_language)
|
||||
|
||||
self._select_language_dialog = None
|
||||
|
||||
def _show_driver_camera(self):
|
||||
if not self._driver_camera:
|
||||
self._driver_camera = DriverCameraDialog()
|
||||
|
||||
gui_app.set_modal_overlay(self._driver_camera, callback=lambda result: setattr(self, '_driver_camera', None))
|
||||
|
||||
def _reset_calibration_prompt(self):
|
||||
if ui_state.engaged:
|
||||
gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Reset Calibration"))
|
||||
return
|
||||
|
||||
gui_app.set_modal_overlay(
|
||||
lambda: confirm_dialog("Are you sure you want to reset calibration?", "Reset"),
|
||||
callback=self._reset_calibration,
|
||||
)
|
||||
|
||||
def _reset_calibration(self, result: int):
|
||||
if ui_state.engaged or result != DialogResult.CONFIRM:
|
||||
return
|
||||
|
||||
self._params.remove("CalibrationParams")
|
||||
self._params.remove("LiveTorqueParameters")
|
||||
self._params.remove("LiveParameters")
|
||||
self._params.remove("LiveParametersV2")
|
||||
self._params.remove("LiveDelay")
|
||||
self._params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def _reboot_prompt(self):
|
||||
if ui_state.engaged:
|
||||
gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Reboot"))
|
||||
return
|
||||
|
||||
gui_app.set_modal_overlay(
|
||||
lambda: confirm_dialog("Are you sure you want to reboot?", "Reboot"),
|
||||
callback=self._perform_reboot,
|
||||
)
|
||||
|
||||
def _perform_reboot(self, result: int):
|
||||
if not ui_state.engaged and result == DialogResult.CONFIRM:
|
||||
self._params.put_bool_nonblocking("DoReboot", True)
|
||||
|
||||
def _power_off_prompt(self):
|
||||
if ui_state.engaged:
|
||||
gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Power Off"))
|
||||
return
|
||||
|
||||
gui_app.set_modal_overlay(
|
||||
lambda: confirm_dialog("Are you sure you want to power off?", "Power Off"),
|
||||
callback=self._perform_power_off,
|
||||
)
|
||||
|
||||
def _perform_power_off(self, result: int):
|
||||
if not ui_state.engaged and result == DialogResult.CONFIRM:
|
||||
self._params.put_bool_nonblocking("DoShutdown", True)
|
||||
|
||||
def _pair_device(self):
|
||||
if not self._pair_device_dialog:
|
||||
self._pair_device_dialog = PairingDialog()
|
||||
gui_app.set_modal_overlay(self._pair_device_dialog, callback=lambda result: setattr(self, '_pair_device_dialog', None))
|
||||
|
||||
def _on_regulatory(self):
|
||||
if not self._fcc_dialog:
|
||||
self._fcc_dialog = HtmlRenderer(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html"))
|
||||
|
||||
gui_app.set_modal_overlay(self._fcc_dialog,
|
||||
callback=lambda result: setattr(self, '_fcc_dialog', None),
|
||||
)
|
||||
|
||||
def _on_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_regulatory(self): pass
|
||||
def _on_change_language(self): pass
|
||||
|
||||
180
selfdrive/ui/layouts/settings/firehose.py
Normal file
180
selfdrive/ui/layouts/settings/firehose.py
Normal file
@@ -0,0 +1,180 @@
|
||||
import pyray as rl
|
||||
import json
|
||||
import time
|
||||
import threading
|
||||
|
||||
from openpilot.common.api import Api, api_get
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
|
||||
TITLE = "Firehose Mode"
|
||||
DESCRIPTION = (
|
||||
"openpilot learns to drive by watching humans, like you, drive.\n\n"
|
||||
+ "Firehose Mode allows you to maximize your training data uploads to improve "
|
||||
+ "openpilot's driving models. More data means bigger models, which means better Experimental Mode."
|
||||
)
|
||||
INSTRUCTIONS = (
|
||||
"For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n"
|
||||
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n"
|
||||
+ "Frequently Asked Questions\n\n"
|
||||
+ "Does it matter how or where I drive? Nope, just drive as you normally would.\n\n"
|
||||
+ "Do all of my segments get pulled in Firehose Mode? No, we selectively pull a subset of your segments.\n\n"
|
||||
+ "What's a good USB-C adapter? Any fast phone or laptop charger should be fine.\n\n"
|
||||
+ "Does it matter which software I run? Yes, only upstream openpilot (and particular forks) are able to be used for training."
|
||||
)
|
||||
|
||||
|
||||
class FirehoseLayout(Widget):
|
||||
PARAM_KEY = "ApiCache_FirehoseStats"
|
||||
GREEN = rl.Color(46, 204, 113, 255)
|
||||
RED = rl.Color(231, 76, 60, 255)
|
||||
GRAY = rl.Color(68, 68, 68, 255)
|
||||
LIGHT_GRAY = rl.Color(228, 228, 228, 255)
|
||||
UPDATE_INTERVAL = 30 # seconds
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.params = Params()
|
||||
self.segment_count = self._get_segment_count()
|
||||
self.scroll_panel = GuiScrollPanel()
|
||||
|
||||
self.running = True
|
||||
self.update_thread = threading.Thread(target=self._update_loop, daemon=True)
|
||||
self.update_thread.start()
|
||||
self.last_update_time = 0
|
||||
|
||||
def _get_segment_count(self) -> int:
|
||||
stats = self.params.get(self.PARAM_KEY, encoding='utf8')
|
||||
if not stats:
|
||||
return 0
|
||||
try:
|
||||
return int(json.loads(stats).get("firehose", 0))
|
||||
except Exception:
|
||||
cloudlog.exception(f"Failed to decode firehose stats: {stats}")
|
||||
return 0
|
||||
|
||||
def __del__(self):
|
||||
self.running = False
|
||||
if self.update_thread and self.update_thread.is_alive():
|
||||
self.update_thread.join(timeout=1.0)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# Calculate content dimensions
|
||||
content_width = rect.width - 80
|
||||
content_height = self._calculate_content_height(int(content_width))
|
||||
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
|
||||
|
||||
# Handle scrolling and render with clipping
|
||||
scroll_offset = self.scroll_panel.handle_scroll(rect, content_rect)
|
||||
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
||||
self._render_content(rect, scroll_offset)
|
||||
rl.end_scissor_mode()
|
||||
|
||||
def _calculate_content_height(self, content_width: int) -> int:
|
||||
height = 80 # Top margin
|
||||
|
||||
# Title
|
||||
height += 100 + 40
|
||||
|
||||
# Description
|
||||
desc_font = gui_app.font(FontWeight.NORMAL)
|
||||
desc_lines = wrap_text(desc_font, DESCRIPTION, 45, content_width)
|
||||
height += len(desc_lines) * 45 + 40
|
||||
|
||||
# Status section
|
||||
height += 32 # Separator
|
||||
status_text, _ = self._get_status()
|
||||
status_lines = wrap_text(gui_app.font(FontWeight.BOLD), status_text, 60, content_width)
|
||||
height += len(status_lines) * 60 + 20
|
||||
|
||||
# Contribution count (if available)
|
||||
if self.segment_count > 0:
|
||||
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
|
||||
contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 52, content_width)
|
||||
height += len(contrib_lines) * 52 + 20
|
||||
|
||||
# Instructions section
|
||||
height += 32 # Separator
|
||||
inst_lines = wrap_text(gui_app.font(FontWeight.NORMAL), INSTRUCTIONS, 40, content_width)
|
||||
height += len(inst_lines) * 40 + 40 # Bottom margin
|
||||
|
||||
return height
|
||||
|
||||
def _render_content(self, rect: rl.Rectangle, scroll_offset: rl.Vector2):
|
||||
x = int(rect.x + 40)
|
||||
y = int(rect.y + 40 + scroll_offset.y)
|
||||
w = int(rect.width - 80)
|
||||
|
||||
# Title
|
||||
title_font = gui_app.font(FontWeight.MEDIUM)
|
||||
rl.draw_text_ex(title_font, TITLE, rl.Vector2(x, y), 100, 0, rl.WHITE)
|
||||
y += 140
|
||||
|
||||
# Description
|
||||
y = self._draw_wrapped_text(x, y, w, DESCRIPTION, gui_app.font(FontWeight.NORMAL), 45, rl.WHITE)
|
||||
y += 40
|
||||
|
||||
# Separator
|
||||
rl.draw_rectangle(x, y, w, 2, self.GRAY)
|
||||
y += 30
|
||||
|
||||
# Status
|
||||
status_text, status_color = self._get_status()
|
||||
y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color)
|
||||
y += 20
|
||||
|
||||
# Contribution count (if available)
|
||||
if self.segment_count > 0:
|
||||
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
|
||||
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
|
||||
y += 20
|
||||
|
||||
# Separator
|
||||
rl.draw_rectangle(x, y, w, 2, self.GRAY)
|
||||
y += 30
|
||||
|
||||
# Instructions
|
||||
self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
|
||||
|
||||
def _draw_wrapped_text(self, x, y, width, text, font, size, color):
|
||||
wrapped = wrap_text(font, text, size, width)
|
||||
for line in wrapped:
|
||||
rl.draw_text_ex(font, line, rl.Vector2(x, y), size, 0, color)
|
||||
y += size
|
||||
return y
|
||||
|
||||
def _get_status(self) -> tuple[str, rl.Color]:
|
||||
network_type = ui_state.sm["deviceState"].networkType
|
||||
network_metered = ui_state.sm["deviceState"].networkMetered
|
||||
|
||||
if not network_metered and network_type != 0: # Not metered and connected
|
||||
return "ACTIVE", self.GREEN
|
||||
else:
|
||||
return "INACTIVE: connect to an unmetered network", self.RED
|
||||
|
||||
def _fetch_firehose_stats(self):
|
||||
try:
|
||||
dongle_id = self.params.get("DongleId", encoding='utf8')
|
||||
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
|
||||
return
|
||||
identity_token = Api(dongle_id).get_token()
|
||||
response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
self.segment_count = data.get("firehose", 0)
|
||||
self.params.put(self.PARAM_KEY, json.dumps(data))
|
||||
except Exception as e:
|
||||
cloudlog.error(f"Failed to fetch firehose stats: {e}")
|
||||
|
||||
def _update_loop(self):
|
||||
while self.running:
|
||||
if not ui_state.started:
|
||||
self._fetch_firehose_stats()
|
||||
time.sleep(self.UPDATE_INTERVAL)
|
||||
@@ -2,15 +2,15 @@ import pyray as rl
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
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.device import DeviceLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.label import gui_text_box
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.selfdrive.ui.layouts.network import NetworkLayout
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
|
||||
# Import individual panels
|
||||
|
||||
@@ -18,9 +18,8 @@ SETTINGS_CLOSE_TEXT = "X"
|
||||
# Constants
|
||||
SIDEBAR_WIDTH = 500
|
||||
CLOSE_BTN_SIZE = 200
|
||||
NAV_BTN_HEIGHT = 80
|
||||
NAV_BTN_HEIGHT = 110
|
||||
PANEL_MARGIN = 50
|
||||
SCROLL_SPEED = 30
|
||||
|
||||
# Colors
|
||||
SIDEBAR_COLOR = rl.BLACK
|
||||
@@ -29,7 +28,6 @@ CLOSE_BTN_COLOR = rl.Color(41, 41, 41, 255)
|
||||
CLOSE_BTN_PRESSED = rl.Color(59, 59, 59, 255)
|
||||
TEXT_NORMAL = rl.Color(128, 128, 128, 255)
|
||||
TEXT_SELECTED = rl.Color(255, 255, 255, 255)
|
||||
TEXT_PRESSED = rl.Color(173, 173, 173, 255)
|
||||
|
||||
|
||||
class PanelType(IntEnum):
|
||||
@@ -45,23 +43,22 @@ class PanelType(IntEnum):
|
||||
class PanelInfo:
|
||||
name: str
|
||||
instance: object
|
||||
button_rect: rl.Rectangle
|
||||
button_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
|
||||
class SettingsLayout:
|
||||
class SettingsLayout(Widget):
|
||||
def __init__(self):
|
||||
self._params = Params()
|
||||
super().__init__()
|
||||
self._current_panel = PanelType.DEVICE
|
||||
self._max_scroll = 0.0
|
||||
|
||||
# Panel configuration
|
||||
self._panels = {
|
||||
PanelType.DEVICE: PanelInfo("Device", DeviceLayout(), rl.Rectangle(0, 0, 0, 0)),
|
||||
PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout(), rl.Rectangle(0, 0, 0, 0)),
|
||||
PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout(), rl.Rectangle(0, 0, 0, 0)),
|
||||
PanelType.FIREHOSE: PanelInfo("Firehose", None, rl.Rectangle(0, 0, 0, 0)),
|
||||
PanelType.NETWORK: PanelInfo("Network", NetworkLayout(), rl.Rectangle(0, 0, 0, 0)),
|
||||
PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayout(), rl.Rectangle(0, 0, 0, 0)),
|
||||
PanelType.DEVICE: PanelInfo("Device", DeviceLayout()),
|
||||
PanelType.NETWORK: PanelInfo("Network", NetworkLayout()),
|
||||
PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout()),
|
||||
PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout()),
|
||||
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout()),
|
||||
PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayout()),
|
||||
}
|
||||
|
||||
self._font_medium = gui_app.font(FontWeight.MEDIUM)
|
||||
@@ -73,7 +70,7 @@ class SettingsLayout:
|
||||
def set_callbacks(self, on_close: Callable):
|
||||
self._close_callback = on_close
|
||||
|
||||
def render(self, rect: rl.Rectangle):
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# Calculate layout
|
||||
sidebar_rect = rl.Rectangle(rect.x, rect.y, SIDEBAR_WIDTH, rect.height)
|
||||
panel_rect = rl.Rectangle(rect.x + SIDEBAR_WIDTH, rect.y, rect.width - SIDEBAR_WIDTH, rect.height)
|
||||
@@ -82,9 +79,6 @@ class SettingsLayout:
|
||||
self._draw_sidebar(sidebar_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):
|
||||
rl.draw_rectangle_rec(rect, SIDEBAR_COLOR)
|
||||
|
||||
@@ -109,17 +103,9 @@ class SettingsLayout:
|
||||
self._close_btn_rect = close_btn_rect
|
||||
|
||||
# Navigation buttons
|
||||
nav_start_y = rect.y + 300
|
||||
button_spacing = 20
|
||||
|
||||
i = 0
|
||||
y = rect.y + 300
|
||||
for panel_type, panel_info in self._panels.items():
|
||||
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_rect = rl.Rectangle(rect.x + 50, y, rect.width - 150, NAV_BTN_HEIGHT)
|
||||
|
||||
# Button styling
|
||||
is_selected = panel_type == self._current_panel
|
||||
@@ -133,7 +119,8 @@ class SettingsLayout:
|
||||
|
||||
# Store button rect for click detection
|
||||
panel_info.button_rect = button_rect
|
||||
i += 1
|
||||
|
||||
y += NAV_BTN_HEIGHT
|
||||
|
||||
def _draw_current_panel(self, rect: rl.Rectangle):
|
||||
rl.draw_rectangle_rounded(
|
||||
@@ -144,17 +131,8 @@ class SettingsLayout:
|
||||
panel = self._panels[self._current_panel]
|
||||
if panel.instance:
|
||||
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
|
||||
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
|
||||
if self._close_callback:
|
||||
@@ -164,20 +142,15 @@ class SettingsLayout:
|
||||
# Check navigation buttons
|
||||
for panel_type, panel_info in self._panels.items():
|
||||
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect):
|
||||
self._switch_to_panel(panel_type)
|
||||
self.set_current_panel(panel_type)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _switch_to_panel(self, panel_type: PanelType):
|
||||
def set_current_panel(self, panel_type: PanelType):
|
||||
if panel_type != self._current_panel:
|
||||
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):
|
||||
if self._close_callback:
|
||||
self._close_callback()
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
from openpilot.system.ui.lib.list_view import ListView, button_item, text_item
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.list_view import button_item, text_item
|
||||
from openpilot.system.ui.lib.scroller import Scroller
|
||||
from openpilot.system.ui.lib.widget import Widget, DialogResult
|
||||
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog
|
||||
|
||||
class SoftwareLayout:
|
||||
|
||||
class SoftwareLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._init_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _init_items(self):
|
||||
items = [
|
||||
text_item("Current Version", ""),
|
||||
button_item("Download", "CHECK", callback=self._on_download_update),
|
||||
@@ -9,13 +22,21 @@ class SoftwareLayout:
|
||||
button_item("Target Branch", "SELECT", callback=self._on_select_branch),
|
||||
button_item("Uninstall", "UNINSTALL", callback=self._on_uninstall),
|
||||
]
|
||||
return items
|
||||
|
||||
self._list_widget = ListView(items)
|
||||
|
||||
def render(self, rect):
|
||||
self._list_widget.render(rect)
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def _on_download_update(self): pass
|
||||
def _on_install_update(self): pass
|
||||
def _on_select_branch(self): pass
|
||||
def _on_uninstall(self): 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,4 +1,6 @@
|
||||
from openpilot.system.ui.lib.list_view import ListView, toggle_item
|
||||
from openpilot.system.ui.lib.list_view import multiple_button_item, toggle_item
|
||||
from openpilot.system.ui.lib.scroller import Scroller
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
from openpilot.common.params import Params
|
||||
|
||||
# Description constants
|
||||
@@ -8,9 +10,14 @@ DESCRIPTIONS = {
|
||||
"Your attention is required at all times to use this feature."
|
||||
),
|
||||
"DisengageOnAccelerator": "When enabled, pressing the accelerator pedal will disengage openpilot.",
|
||||
"LongitudinalPersonality": (
|
||||
"Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " +
|
||||
"In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " +
|
||||
"your steering wheel distance button."
|
||||
),
|
||||
"IsLdwEnabled": (
|
||||
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
|
||||
"without a turn signal activated while driving over 31 mph (50 km/h)."
|
||||
"without a turn signal activated while driving over 31 mph (50 km/h)."
|
||||
),
|
||||
"AlwaysOnDM": "Enable driver monitoring even when openpilot is not engaged.",
|
||||
'RecordFront': "Upload data from the driver facing camera and help improve the driver monitoring algorithm.",
|
||||
@@ -18,8 +25,9 @@ DESCRIPTIONS = {
|
||||
}
|
||||
|
||||
|
||||
class TogglesLayout:
|
||||
class TogglesLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
items = [
|
||||
toggle_item(
|
||||
@@ -39,6 +47,15 @@ class TogglesLayout:
|
||||
self._params.get_bool("DisengageOnAccelerator"),
|
||||
icon="disengage_on_accelerator.png",
|
||||
),
|
||||
multiple_button_item(
|
||||
"Driving Personality",
|
||||
DESCRIPTIONS["LongitudinalPersonality"],
|
||||
buttons=["Aggressive", "Standard", "Relaxed"],
|
||||
button_width=255,
|
||||
callback=self._set_longitudinal_personality,
|
||||
selected_index=int(self._params.get("LongitudinalPersonality") or 0),
|
||||
icon="speed_limit.png"
|
||||
),
|
||||
toggle_item(
|
||||
"Enable Lane Departure Warnings",
|
||||
DESCRIPTIONS["IsLdwEnabled"],
|
||||
@@ -62,7 +79,10 @@ class TogglesLayout:
|
||||
),
|
||||
]
|
||||
|
||||
self._list_widget = ListView(items)
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def render(self, rect):
|
||||
self._list_widget.render(rect)
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def _set_longitudinal_personality(self, button_index: int):
|
||||
self._params.put("LongitudinalPersonality", str(button_index))
|
||||
|
||||
@@ -6,6 +6,7 @@ from cereal import log
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
|
||||
SIDEBAR_WIDTH = 300
|
||||
METRIC_HEIGHT = 126
|
||||
@@ -18,6 +19,7 @@ HOME_BTN = rl.Rectangle(60, 860, 180, 180)
|
||||
ThermalStatus = log.DeviceState.ThermalStatus
|
||||
NetworkType = log.DeviceState.NetworkType
|
||||
|
||||
|
||||
# Color scheme
|
||||
class Colors:
|
||||
SIDEBAR_BG = rl.Color(57, 57, 57, 255)
|
||||
@@ -35,6 +37,7 @@ class Colors:
|
||||
BUTTON_NORMAL = rl.Color(255, 255, 255, 255)
|
||||
BUTTON_PRESSED = rl.Color(255, 255, 255, 166)
|
||||
|
||||
|
||||
NETWORK_TYPES = {
|
||||
NetworkType.none: "Offline",
|
||||
NetworkType.wifi: "WiFi",
|
||||
@@ -57,8 +60,10 @@ class MetricData:
|
||||
self.value = value
|
||||
self.color = color
|
||||
|
||||
class Sidebar:
|
||||
|
||||
class Sidebar(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._net_type = NETWORK_TYPES.get(NetworkType.none)
|
||||
self._net_strength = 0
|
||||
|
||||
@@ -72,7 +77,7 @@ class Sidebar:
|
||||
self._font_regular = gui_app.font(FontWeight.NORMAL)
|
||||
self._font_bold = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
|
||||
# Callbacks
|
||||
# Callbacks
|
||||
self._on_settings_click: Callable | None = None
|
||||
self._on_flag_click: Callable | None = None
|
||||
|
||||
@@ -80,9 +85,7 @@ class Sidebar:
|
||||
self._on_settings_click = on_settings
|
||||
self._on_flag_click = on_flag
|
||||
|
||||
def render(self, rect: rl.Rectangle):
|
||||
self.update_state()
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# Background
|
||||
rl.draw_rectangle_rec(rect, Colors.SIDEBAR_BG)
|
||||
|
||||
@@ -90,9 +93,7 @@ class Sidebar:
|
||||
self._draw_network_indicator(rect)
|
||||
self._draw_metrics(rect)
|
||||
|
||||
self._handle_mouse_release()
|
||||
|
||||
def update_state(self):
|
||||
def _update_state(self):
|
||||
sm = ui_state.sm
|
||||
if not sm.updated['deviceState']:
|
||||
return
|
||||
@@ -134,11 +135,7 @@ class Sidebar:
|
||||
else:
|
||||
self._panda_status.update("VEHICLE", "ONLINE", Colors.GOOD)
|
||||
|
||||
def _handle_mouse_release(self):
|
||||
if not rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||
return
|
||||
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
def _handle_mouse_release(self, mouse_pos: rl.Vector2):
|
||||
if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN):
|
||||
if self._on_settings_click:
|
||||
self._on_settings_click()
|
||||
@@ -148,8 +145,7 @@ class Sidebar:
|
||||
|
||||
def _draw_buttons(self, rect: rl.Rectangle):
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
||||
|
||||
mouse_down = self._is_pressed and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
||||
|
||||
# Settings button
|
||||
settings_down = mouse_down and rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN)
|
||||
|
||||
99
selfdrive/ui/lib/prime_state.py
Normal file
99
selfdrive/ui/lib/prime_state.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from enum import IntEnum
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
from openpilot.common.api import Api, api_get
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
|
||||
|
||||
|
||||
class PrimeType(IntEnum):
|
||||
UNKNOWN = -2,
|
||||
UNPAIRED = -1,
|
||||
NONE = 0,
|
||||
MAGENTA = 1,
|
||||
LITE = 2,
|
||||
BLUE = 3,
|
||||
MAGENTA_NEW = 4,
|
||||
PURPLE = 5,
|
||||
|
||||
|
||||
class PrimeState:
|
||||
FETCH_INTERVAL = 5.0 # seconds between API calls
|
||||
API_TIMEOUT = 10.0 # seconds for API requests
|
||||
SLEEP_INTERVAL = 0.5 # seconds to sleep between checks in the worker thread
|
||||
|
||||
def __init__(self):
|
||||
self._params = Params()
|
||||
self._lock = threading.Lock()
|
||||
self.prime_type: PrimeType = self._load_initial_state()
|
||||
|
||||
self._running = False
|
||||
self._thread = None
|
||||
self.start()
|
||||
|
||||
def _load_initial_state(self) -> PrimeType:
|
||||
prime_type_str = os.getenv("PRIME_TYPE") or self._params.get("PrimeType", encoding='utf8')
|
||||
try:
|
||||
if prime_type_str is not None:
|
||||
return PrimeType(int(prime_type_str))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return PrimeType.UNKNOWN
|
||||
|
||||
def _fetch_prime_status(self) -> None:
|
||||
dongle_id = self._params.get("DongleId", encoding='utf8')
|
||||
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
|
||||
return
|
||||
|
||||
try:
|
||||
identity_token = Api(dongle_id).get_token()
|
||||
response = api_get(f"v1.1/devices/{dongle_id}", timeout=self.API_TIMEOUT, access_token=identity_token)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
is_paired = data.get("is_paired", False)
|
||||
prime_type = data.get("prime_type", 0)
|
||||
self.set_type(PrimeType(prime_type) if is_paired else PrimeType.UNPAIRED)
|
||||
except Exception as e:
|
||||
cloudlog.error(f"Failed to fetch prime status: {e}")
|
||||
|
||||
def set_type(self, prime_type: PrimeType) -> None:
|
||||
with self._lock:
|
||||
if prime_type != self.prime_type:
|
||||
self.prime_type = prime_type
|
||||
self._params.put("PrimeType", str(int(prime_type)))
|
||||
cloudlog.info(f"Prime type updated to {prime_type}")
|
||||
|
||||
def _worker_thread(self) -> None:
|
||||
while self._running:
|
||||
self._fetch_prime_status()
|
||||
|
||||
for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)):
|
||||
if not self._running:
|
||||
break
|
||||
time.sleep(self.SLEEP_INTERVAL)
|
||||
|
||||
def start(self) -> None:
|
||||
if self._thread and self._thread.is_alive():
|
||||
return
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._worker_thread, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def stop(self) -> None:
|
||||
self._running = False
|
||||
if self._thread and self._thread.is_alive():
|
||||
self._thread.join(timeout=1.0)
|
||||
|
||||
def get_type(self) -> PrimeType:
|
||||
with self._lock:
|
||||
return self.prime_type
|
||||
|
||||
def is_prime(self) -> bool:
|
||||
with self._lock:
|
||||
return bool(self.prime_type > PrimeType.NONE)
|
||||
|
||||
def __del__(self):
|
||||
self.stop()
|
||||
@@ -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.label import gui_text_box
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
|
||||
ALERT_MARGIN = 40
|
||||
ALERT_PADDING = 60
|
||||
ALERT_LINE_SPACING = 45
|
||||
@@ -21,7 +21,6 @@ ALERT_FONT_BIG = 88
|
||||
SELFDRIVE_STATE_TIMEOUT = 5 # Seconds
|
||||
SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 # Seconds
|
||||
|
||||
|
||||
# Constants
|
||||
ALERT_COLORS = {
|
||||
log.SelfdriveState.AlertStatus.normal: rl.Color(0, 0, 0, 235), # Black
|
||||
@@ -61,8 +60,9 @@ ALERT_CRITICAL_REBOOT = Alert(
|
||||
)
|
||||
|
||||
|
||||
class AlertRenderer:
|
||||
class AlertRenderer(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.font_regular: rl.Font = gui_app.font(FontWeight.NORMAL)
|
||||
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||
|
||||
@@ -73,18 +73,20 @@ class AlertRenderer:
|
||||
# Check if selfdriveState messages have stopped arriving
|
||||
if not sm.updated['selfdriveState']:
|
||||
recv_frame = sm.recv_frame['selfdriveState']
|
||||
if (sm.frame - recv_frame) > 5 * DEFAULT_FPS:
|
||||
# Check if waiting to start
|
||||
if recv_frame < ui_state.started_frame:
|
||||
return ALERT_STARTUP_PENDING
|
||||
time_since_onroad = (sm.frame - ui_state.started_frame) / DEFAULT_FPS
|
||||
|
||||
# Handle selfdrive timeout
|
||||
if TICI:
|
||||
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
|
||||
# 1. Never received selfdriveState since going onroad
|
||||
waiting_for_startup = recv_frame < ui_state.started_frame
|
||||
if waiting_for_startup and time_since_onroad > 5:
|
||||
return ALERT_STARTUP_PENDING
|
||||
|
||||
# 2. Lost communication with selfdriveState after receiving it
|
||||
if TICI and not waiting_for_startup:
|
||||
ss_missing = time.monotonic() - sm.recv_time['selfdriveState']
|
||||
if ss_missing > SELFDRIVE_STATE_TIMEOUT:
|
||||
if ss.enabled and (ss_missing - SELFDRIVE_STATE_TIMEOUT) < SELFDRIVE_UNRESPONSIVE_TIMEOUT:
|
||||
return ALERT_CRITICAL_TIMEOUT
|
||||
return ALERT_CRITICAL_REBOOT
|
||||
|
||||
# No alert if size is none
|
||||
if ss.alertSize == 0:
|
||||
@@ -93,10 +95,10 @@ class AlertRenderer:
|
||||
# Return current alert
|
||||
return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize, status=ss.alertStatus)
|
||||
|
||||
def draw(self, rect: rl.Rectangle, sm: messaging.SubMaster) -> None:
|
||||
alert = self.get_alert(sm)
|
||||
def _render(self, rect: rl.Rectangle) -> bool:
|
||||
alert = self.get_alert(ui_state.sm)
|
||||
if not alert:
|
||||
return
|
||||
return False
|
||||
|
||||
alert_rect = self._get_alert_rect(rect, alert.size)
|
||||
self._draw_background(alert_rect, alert)
|
||||
@@ -108,13 +110,14 @@ class AlertRenderer:
|
||||
alert_rect.height - 2 * ALERT_PADDING
|
||||
)
|
||||
self._draw_text(text_rect, alert)
|
||||
return True
|
||||
|
||||
def _get_alert_rect(self, rect: rl.Rectangle, size: int) -> rl.Rectangle:
|
||||
if size == log.SelfdriveState.AlertSize.full:
|
||||
return rect
|
||||
|
||||
height = (ALERT_FONT_MEDIUM + 2 * ALERT_PADDING if size == log.SelfdriveState.AlertSize.small else
|
||||
ALERT_FONT_BIG + ALERT_LINE_SPACING + ALERT_FONT_SMALL + 2 * ALERT_PADDING)
|
||||
ALERT_FONT_BIG + ALERT_LINE_SPACING + ALERT_FONT_SMALL + 2 * ALERT_PADDING)
|
||||
|
||||
return rl.Rectangle(
|
||||
rect.x + ALERT_MARGIN,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
|
||||
from collections.abc import Callable
|
||||
from cereal import log
|
||||
from msgq.visionipc import VisionStreamType
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, UI_BORDER_SIZE
|
||||
@@ -13,7 +13,6 @@ from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame
|
||||
from openpilot.common.transformations.orientation import rot_from_euler
|
||||
|
||||
|
||||
OpState = log.SelfdriveState.OpenpilotState
|
||||
CALIBRATED = log.LiveCalibrationData.Status.calibrated
|
||||
ROAD_CAM = VisionStreamType.VISION_STREAM_ROAD
|
||||
@@ -26,6 +25,9 @@ BORDER_COLORS = {
|
||||
UIStatus.ENGAGED: rl.Color(0x17, 0x86, 0x44, 0xF1), # Green for engaged state
|
||||
}
|
||||
|
||||
WIDE_CAM_MAX_SPEED = 10.0 # m/s (22 mph)
|
||||
ROAD_CAM_MIN_SPEED = 15.0 # m/s (34 mph)
|
||||
|
||||
|
||||
class AugmentedRoadView(CameraView):
|
||||
def __init__(self, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_ROAD):
|
||||
@@ -47,11 +49,19 @@ class AugmentedRoadView(CameraView):
|
||||
self.alert_renderer = AlertRenderer()
|
||||
self.driver_state_renderer = DriverStateRenderer()
|
||||
|
||||
def render(self, rect):
|
||||
# Callbacks
|
||||
self._click_callback: Callable | None = None
|
||||
|
||||
def set_callbacks(self, on_click: Callable | None = None):
|
||||
self._click_callback = on_click
|
||||
|
||||
def _render(self, rect):
|
||||
# Only render when system is started to avoid invalid data access
|
||||
if not ui_state.started:
|
||||
return
|
||||
|
||||
self._switch_stream_if_needed(ui_state.sm)
|
||||
|
||||
# Update calibration before rendering
|
||||
self._update_calibration()
|
||||
|
||||
@@ -76,13 +86,13 @@ class AugmentedRoadView(CameraView):
|
||||
)
|
||||
|
||||
# Render the base camera view
|
||||
super().render(rect)
|
||||
super()._render(rect)
|
||||
|
||||
# Draw all UI overlays
|
||||
self.model_renderer.draw(self._content_rect, ui_state.sm)
|
||||
self._hud_renderer.draw(self._content_rect, ui_state.sm)
|
||||
self.alert_renderer.draw(self._content_rect, ui_state.sm)
|
||||
self.driver_state_renderer.draw(self._content_rect, ui_state.sm)
|
||||
self.model_renderer.render(self._content_rect)
|
||||
self._hud_renderer.render(self._content_rect)
|
||||
if not self.alert_renderer.render(self._content_rect):
|
||||
self.driver_state_renderer.render(self._content_rect)
|
||||
|
||||
# Custom UI extension point - add custom overlays here
|
||||
# Use self._content_rect for positioning within camera bounds
|
||||
@@ -90,10 +100,32 @@ class AugmentedRoadView(CameraView):
|
||||
# End clipping region
|
||||
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):
|
||||
border_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED])
|
||||
rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, border_color)
|
||||
|
||||
def _switch_stream_if_needed(self, sm):
|
||||
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
|
||||
v_ego = sm['carState'].vEgo
|
||||
if v_ego < WIDE_CAM_MAX_SPEED:
|
||||
target = WIDE_CAM
|
||||
elif v_ego > ROAD_CAM_MIN_SPEED:
|
||||
target = ROAD_CAM
|
||||
else:
|
||||
# Hysteresis zone - keep current stream
|
||||
target = self.stream_type
|
||||
else:
|
||||
target = ROAD_CAM
|
||||
|
||||
if self.stream_type != target:
|
||||
self.switch_stream(target)
|
||||
|
||||
def _update_calibration(self):
|
||||
# Update device camera if not already set
|
||||
sm = ui_state.sm
|
||||
@@ -129,7 +161,7 @@ class AugmentedRoadView(CameraView):
|
||||
|
||||
# Get camera configuration
|
||||
device_camera = self.device_camera or DEFAULT_DEVICE_CAMERA
|
||||
is_wide_camera = self.stream_type == VisionStreamType.VISION_STREAM_WIDE_ROAD
|
||||
is_wide_camera = self.stream_type == WIDE_CAM
|
||||
intrinsic = device_camera.ecam.intrinsics if is_wide_camera else device_camera.fcam.intrinsics
|
||||
calibration = self.view_from_wide_calib if is_wide_camera else self.view_from_calib
|
||||
zoom = 2.0 if is_wide_camera else 1.1
|
||||
@@ -170,9 +202,9 @@ class AugmentedRoadView(CameraView):
|
||||
])
|
||||
|
||||
video_transform = np.array([
|
||||
[zoom, 0.0, (w / 2 + x - x_offset) - (cx * zoom)],
|
||||
[0.0, zoom, (h / 2 + y - y_offset) - (cy * zoom)],
|
||||
[0.0, 0.0, 1.0]
|
||||
[zoom, 0.0, (w / 2 + x - x_offset) - (cx * zoom)],
|
||||
[0.0, zoom, (h / 2 + y - y_offset) - (cy * zoom)],
|
||||
[0.0, 0.0, 1.0]
|
||||
])
|
||||
self.model_renderer.set_transform(video_transform @ calib_transform)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import platform
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
|
||||
@@ -6,12 +7,21 @@ from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage
|
||||
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
|
||||
CONNECTION_RETRY_INTERVAL = 0.2 # seconds between connection attempts
|
||||
|
||||
VERTEX_SHADER = """
|
||||
VERSION = """
|
||||
#version 300 es
|
||||
precision mediump float;
|
||||
"""
|
||||
if platform.system() == "Darwin":
|
||||
VERSION = """
|
||||
#version 330 core
|
||||
"""
|
||||
|
||||
|
||||
VERTEX_SHADER = VERSION + """
|
||||
in vec3 vertexPosition;
|
||||
in vec2 vertexTexCoord;
|
||||
in vec3 vertexNormal;
|
||||
@@ -41,9 +51,7 @@ if TICI:
|
||||
}
|
||||
"""
|
||||
else:
|
||||
FRAME_FRAGMENT_SHADER = """
|
||||
#version 300 es
|
||||
precision mediump float;
|
||||
FRAME_FRAGMENT_SHADER = VERSION + """
|
||||
in vec2 fragTexCoord;
|
||||
uniform sampler2D texture0;
|
||||
uniform sampler2D texture1;
|
||||
@@ -55,8 +63,10 @@ else:
|
||||
}
|
||||
"""
|
||||
|
||||
class CameraView:
|
||||
|
||||
class CameraView(Widget):
|
||||
def __init__(self, name: str, stream_type: VisionStreamType):
|
||||
super().__init__()
|
||||
self._name = name
|
||||
# Primary stream
|
||||
self.client = VisionIpcClient(name, stream_type, conflate=True)
|
||||
@@ -68,7 +78,6 @@ class CameraView:
|
||||
self._target_stream_type: VisionStreamType | None = None
|
||||
self._switching: bool = False
|
||||
|
||||
|
||||
self._texture_needs_update = True
|
||||
self.last_connection_attempt: float = 0.0
|
||||
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAME_FRAGMENT_SHADER)
|
||||
@@ -82,7 +91,7 @@ class CameraView:
|
||||
self.egl_images: dict[int, EGLImage] = {}
|
||||
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
|
||||
if TICI:
|
||||
@@ -145,12 +154,12 @@ class CameraView:
|
||||
zy = min(widget_aspect_ratio / frame_aspect_ratio, 1.0)
|
||||
|
||||
return np.array([
|
||||
[zx, 0.0, 0.0],
|
||||
[0.0, zy, 0.0],
|
||||
[0.0, 0.0, 1.0]
|
||||
[zx, 0.0, 0.0],
|
||||
[0.0, zy, 0.0],
|
||||
[0.0, 0.0, 1.0]
|
||||
])
|
||||
|
||||
def render(self, rect: rl.Rectangle):
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if self._switching:
|
||||
self._handle_switch()
|
||||
|
||||
@@ -230,7 +239,7 @@ class CameraView:
|
||||
# Update textures with new frame data
|
||||
if self._texture_needs_update:
|
||||
y_data = self.frame.data[: self.frame.uv_offset]
|
||||
uv_data = self.frame.data[self.frame.uv_offset :]
|
||||
uv_data = self.frame.data[self.frame.uv_offset:]
|
||||
|
||||
rl.update_texture(self.texture_y, rl.ffi.cast("void *", y_data.ctypes.data))
|
||||
rl.update_texture(self.texture_uv, rl.ffi.cast("void *", uv_data.ctypes.data))
|
||||
@@ -265,7 +274,7 @@ class CameraView:
|
||||
def _handle_switch(self) -> None:
|
||||
"""Check if target stream is ready and switch immediately."""
|
||||
if not self._target_client or not self._switching:
|
||||
return
|
||||
return
|
||||
|
||||
# Try to connect target if needed
|
||||
if not self._target_client.is_connected():
|
||||
@@ -277,28 +286,28 @@ class CameraView:
|
||||
# Check if target has frames ready
|
||||
target_frame = self._target_client.recv(timeout_ms=0)
|
||||
if target_frame:
|
||||
self.frame = target_frame # Update current frame to target frame
|
||||
self.frame = target_frame # Update current frame to target frame
|
||||
self._complete_switch()
|
||||
|
||||
def _complete_switch(self) -> None:
|
||||
"""Instantly switch to target stream."""
|
||||
cloudlog.debug(f"Switching to {self._target_stream_type}")
|
||||
# Clean up current resources
|
||||
if self.client:
|
||||
del self.client
|
||||
"""Instantly switch to target stream."""
|
||||
cloudlog.debug(f"Switching to {self._target_stream_type}")
|
||||
# Clean up current resources
|
||||
if self.client:
|
||||
del self.client
|
||||
|
||||
# Switch to target
|
||||
self.client = self._target_client
|
||||
self._stream_type = self._target_stream_type
|
||||
self._texture_needs_update = True
|
||||
# Switch to target
|
||||
self.client = self._target_client
|
||||
self._stream_type = self._target_stream_type
|
||||
self._texture_needs_update = True
|
||||
|
||||
# Reset state
|
||||
self._target_client = None
|
||||
self._target_stream_type = None
|
||||
self._switching = False
|
||||
# Reset state
|
||||
self._target_client = None
|
||||
self._target_stream_type = None
|
||||
self._switching = False
|
||||
|
||||
# Initialize textures for new stream
|
||||
self._initialize_textures()
|
||||
# Initialize textures for new stream
|
||||
self._initialize_textures()
|
||||
|
||||
def _initialize_textures(self):
|
||||
self._clear_textures()
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
from cereal import messaging
|
||||
from msgq.visionipc import VisionStreamType
|
||||
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
|
||||
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.label import gui_label
|
||||
|
||||
|
||||
class DriverCameraView(CameraView):
|
||||
def __init__(self, stream_type: VisionStreamType):
|
||||
super().__init__("camerad", stream_type)
|
||||
class DriverCameraDialog(CameraView):
|
||||
def __init__(self):
|
||||
super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER)
|
||||
self.driver_state_renderer = DriverStateRenderer()
|
||||
|
||||
def render(self, rect, sm):
|
||||
super().render(rect)
|
||||
def _render(self, rect):
|
||||
super()._render(rect)
|
||||
|
||||
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||
return 1
|
||||
|
||||
if not self.frame:
|
||||
gui_label(
|
||||
@@ -24,13 +27,15 @@ class DriverCameraView(CameraView):
|
||||
font_weight=FontWeight.BOLD,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
)
|
||||
return
|
||||
return -1
|
||||
|
||||
self._draw_face_detection(rect, sm)
|
||||
self.driver_state_renderer.draw(rect, sm)
|
||||
self._draw_face_detection(rect)
|
||||
self.driver_state_renderer.render(rect)
|
||||
|
||||
def _draw_face_detection(self, rect: rl.Rectangle, sm) -> None:
|
||||
driver_state = sm["driverStateV2"]
|
||||
return -1
|
||||
|
||||
def _draw_face_detection(self, rect: rl.Rectangle) -> None:
|
||||
driver_state = ui_state.sm["driverStateV2"]
|
||||
is_rhd = driver_state.wheelOnRightProb > 0.5
|
||||
driver_data = driver_state.rightDriverData if is_rhd else driver_state.leftDriverData
|
||||
face_detect = driver_data.faceProb > 0.7
|
||||
@@ -83,12 +88,11 @@ class DriverCameraView(CameraView):
|
||||
|
||||
if __name__ == "__main__":
|
||||
gui_app.init_window("Driver Camera View")
|
||||
sm = messaging.SubMaster(["selfdriveState", "driverStateV2", "driverMonitoringState"])
|
||||
|
||||
driver_camera_view = DriverCameraView(VisionStreamType.VISION_STREAM_DRIVER)
|
||||
driver_camera_view = DriverCameraDialog()
|
||||
try:
|
||||
for _ in gui_app.render():
|
||||
sm.update()
|
||||
driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height), sm)
|
||||
ui_state.update()
|
||||
driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
finally:
|
||||
driver_camera_view.close()
|
||||
@@ -3,19 +3,19 @@ import pyray as rl
|
||||
from dataclasses import dataclass
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UI_BORDER_SIZE
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
|
||||
# Default 3D coordinates for face keypoints as a NumPy array
|
||||
DEFAULT_FACE_KPTS_3D = np.array([
|
||||
[-5.98, -51.20, 8.00], [-17.64, -49.14, 8.00], [-23.81, -46.40, 8.00], [-29.98, -40.91, 8.00],
|
||||
[-32.04, -37.49, 8.00], [-34.10, -32.00, 8.00], [-36.16, -21.03, 8.00], [-36.16, 6.40, 8.00],
|
||||
[-35.47, 10.51, 8.00], [-32.73, 19.43, 8.00], [-29.30, 26.29, 8.00], [-24.50, 33.83, 8.00],
|
||||
[-19.01, 41.37, 8.00], [-14.21, 46.17, 8.00], [-12.16, 47.54, 8.00], [-4.61, 49.60, 8.00],
|
||||
[4.99, 49.60, 8.00], [12.53, 47.54, 8.00], [14.59, 46.17, 8.00], [19.39, 41.37, 8.00],
|
||||
[24.87, 33.83, 8.00], [29.67, 26.29, 8.00], [33.10, 19.43, 8.00], [35.84, 10.51, 8.00],
|
||||
[36.53, 6.40, 8.00], [36.53, -21.03, 8.00], [34.47, -32.00, 8.00], [32.42, -37.49, 8.00],
|
||||
[30.36, -40.91, 8.00], [24.19, -46.40, 8.00], [18.02, -49.14, 8.00], [6.36, -51.20, 8.00],
|
||||
[-5.98, -51.20, 8.00],
|
||||
[-5.98, -51.20, 8.00], [-17.64, -49.14, 8.00], [-23.81, -46.40, 8.00], [-29.98, -40.91, 8.00],
|
||||
[-32.04, -37.49, 8.00], [-34.10, -32.00, 8.00], [-36.16, -21.03, 8.00], [-36.16, 6.40, 8.00],
|
||||
[-35.47, 10.51, 8.00], [-32.73, 19.43, 8.00], [-29.30, 26.29, 8.00], [-24.50, 33.83, 8.00],
|
||||
[-19.01, 41.37, 8.00], [-14.21, 46.17, 8.00], [-12.16, 47.54, 8.00], [-4.61, 49.60, 8.00],
|
||||
[4.99, 49.60, 8.00], [12.53, 47.54, 8.00], [14.59, 46.17, 8.00], [19.39, 41.37, 8.00],
|
||||
[24.87, 33.83, 8.00], [29.67, 26.29, 8.00], [33.10, 19.43, 8.00], [35.84, 10.51, 8.00],
|
||||
[36.53, 6.40, 8.00], [36.53, -21.03, 8.00], [34.47, -32.00, 8.00], [32.42, -37.49, 8.00],
|
||||
[30.36, -40.91, 8.00], [24.19, -46.40, 8.00], [18.02, -49.14, 8.00], [6.36, -51.20, 8.00],
|
||||
[-5.98, -51.20, 8.00],
|
||||
], dtype=np.float32)
|
||||
|
||||
# UI constants
|
||||
@@ -31,6 +31,7 @@ SCALES_NEG = np.array([0.7, 0.4, 0.4], dtype=np.float32)
|
||||
ARC_POINT_COUNT = 37 # Number of points in the arc
|
||||
ARC_ANGLES = np.linspace(0.0, np.pi, ARC_POINT_COUNT, dtype=np.float32)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArcData:
|
||||
"""Data structure for arc rendering parameters."""
|
||||
@@ -40,14 +41,15 @@ class ArcData:
|
||||
height: float
|
||||
thickness: float
|
||||
|
||||
class DriverStateRenderer:
|
||||
|
||||
class DriverStateRenderer(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Initial state with NumPy arrays
|
||||
self.face_kpts_draw = DEFAULT_FACE_KPTS_3D.copy()
|
||||
self.is_active = False
|
||||
self.is_rhd = False
|
||||
self.dm_fade_state = 0.0
|
||||
self.state_updated = False
|
||||
self.last_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
|
||||
self.driver_pose_vals = np.zeros(3, dtype=np.float32)
|
||||
self.driver_pose_diff = np.zeros(3, dtype=np.float32)
|
||||
@@ -73,14 +75,10 @@ class DriverStateRenderer:
|
||||
self.engaged_color = rl.Color(26, 242, 66, 255)
|
||||
self.disengaged_color = rl.Color(139, 139, 139, 255)
|
||||
|
||||
def draw(self, rect, sm):
|
||||
if not self._is_visible(sm):
|
||||
return
|
||||
|
||||
self._update_state(sm, rect)
|
||||
if not self.state_updated:
|
||||
return
|
||||
self.set_visible(lambda: (ui_state.sm.recv_frame['driverStateV2'] > ui_state.started_frame and
|
||||
ui_state.sm.seen['driverMonitoringState']))
|
||||
|
||||
def _render(self, rect):
|
||||
# Set opacity based on active state
|
||||
opacity = 0.65 if self.is_active else 0.2
|
||||
|
||||
@@ -105,18 +103,14 @@ class DriverStateRenderer:
|
||||
if self.v_arc_data:
|
||||
rl.draw_spline_linear(self.v_arc_lines, len(self.v_arc_lines), self.v_arc_data.thickness, self.arc_color)
|
||||
|
||||
def _is_visible(self, sm):
|
||||
"""Check if the visualization should be rendered."""
|
||||
return (sm.recv_frame['driverStateV2'] > ui_state.started_frame and
|
||||
sm.seen['driverMonitoringState'] and
|
||||
sm['selfdriveState'].alertSize == 0)
|
||||
|
||||
def _update_state(self, sm, rect):
|
||||
def _update_state(self):
|
||||
"""Update the driver monitoring state based on model data"""
|
||||
if not sm.updated["driverMonitoringState"]:
|
||||
if self.state_updated and (rect.x != self.last_rect.x or rect.y != self.last_rect.y or \
|
||||
rect.width != self.last_rect.width or rect.height != self.last_rect.height):
|
||||
self._pre_calculate_drawing_elements(rect)
|
||||
sm = ui_state.sm
|
||||
if not sm.updated["driverMonitoringState"]:
|
||||
if (self._rect.x != self.last_rect.x or self._rect.y != self.last_rect.y or
|
||||
self._rect.width != self.last_rect.width or self._rect.height != self.last_rect.height):
|
||||
self._pre_calculate_drawing_elements()
|
||||
self.last_rect = self._rect
|
||||
return
|
||||
|
||||
# Get monitoring state
|
||||
@@ -165,16 +159,15 @@ class DriverStateRenderer:
|
||||
self.face_keypoints_transformed = self.face_kpts_draw[:, :2] * kp_depth[:, None]
|
||||
|
||||
# Pre-calculate all drawing elements
|
||||
self._pre_calculate_drawing_elements(rect)
|
||||
self.state_updated = True
|
||||
self._pre_calculate_drawing_elements()
|
||||
|
||||
def _pre_calculate_drawing_elements(self, rect):
|
||||
def _pre_calculate_drawing_elements(self):
|
||||
"""Pre-calculate all drawing elements based on the current rectangle"""
|
||||
# Calculate icon position (bottom-left or bottom-right)
|
||||
width, height = rect.width, rect.height
|
||||
width, height = self._rect.width, self._rect.height
|
||||
offset = UI_BORDER_SIZE + BTN_SIZE // 2
|
||||
self.position_x = rect.x + (width - offset if self.is_rhd else offset)
|
||||
self.position_y = rect.y + height - offset
|
||||
self.position_x = self._rect.x + (width - offset if self.is_rhd else offset)
|
||||
self.position_y = self._rect.y + height - offset
|
||||
|
||||
# Pre-calculate the face lines positions
|
||||
positioned_keypoints = self.face_keypoints_transformed + np.array([self.position_x, self.position_y])
|
||||
@@ -189,15 +182,15 @@ class DriverStateRenderer:
|
||||
# Horizontal arc
|
||||
h_width = abs(delta_x)
|
||||
self.h_arc_data = self._calculate_arc_data(
|
||||
delta_x, h_width, self.position_x, self.position_y - ARC_LENGTH / 2,
|
||||
self.driver_pose_sins[1], self.driver_pose_diff[1], is_horizontal=True
|
||||
delta_x, h_width, self.position_x, self.position_y - ARC_LENGTH / 2,
|
||||
self.driver_pose_sins[1], self.driver_pose_diff[1], is_horizontal=True
|
||||
)
|
||||
|
||||
# Vertical arc
|
||||
v_height = abs(delta_y)
|
||||
self.v_arc_data = self._calculate_arc_data(
|
||||
delta_y, v_height, self.position_x - ARC_LENGTH / 2, self.position_y,
|
||||
self.driver_pose_sins[0], self.driver_pose_diff[0], is_horizontal=False
|
||||
delta_y, v_height, self.position_x - ARC_LENGTH / 2, self.position_y,
|
||||
self.driver_pose_sins[0], self.driver_pose_diff[0], is_horizontal=False
|
||||
)
|
||||
|
||||
def _calculate_arc_data(
|
||||
|
||||
78
selfdrive/ui/onroad/exp_button.py
Normal file
78
selfdrive/ui/onroad/exp_button.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import time
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
from openpilot.common.params import Params
|
||||
|
||||
|
||||
class ExpButton(Widget):
|
||||
def __init__(self, button_size: int, icon_size: int):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._experimental_mode: bool = False
|
||||
self._engageable: bool = False
|
||||
|
||||
# State hold mechanism
|
||||
self._hold_duration = 2.0 # seconds
|
||||
self._held_mode: bool | None = None
|
||||
self._hold_end_time: float | None = None
|
||||
|
||||
self._white_color: rl.Color = rl.Color(255, 255, 255, 255)
|
||||
self._black_bg: rl.Color = rl.Color(0, 0, 0, 166)
|
||||
self._txt_wheel: rl.Texture = gui_app.texture('icons/chffr_wheel.png', icon_size, icon_size)
|
||||
self._txt_exp: rl.Texture = gui_app.texture('icons/experimental.png', icon_size, icon_size)
|
||||
self._rect = rl.Rectangle(0, 0, button_size, button_size)
|
||||
|
||||
def set_rect(self, rect: rl.Rectangle) -> None:
|
||||
self._rect.x, self._rect.y = rect.x, rect.y
|
||||
|
||||
def _update_state(self) -> None:
|
||||
selfdrive_state = ui_state.sm["selfdriveState"]
|
||||
self._experimental_mode = selfdrive_state.experimentalMode
|
||||
self._engageable = selfdrive_state.engageable or selfdrive_state.enabled
|
||||
|
||||
def handle_mouse_event(self) -> bool:
|
||||
if rl.check_collision_point_rec(rl.get_mouse_position(), self._rect):
|
||||
if (rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and
|
||||
self._is_toggle_allowed()):
|
||||
new_mode = not self._experimental_mode
|
||||
self._params.put_bool("ExperimentalMode", new_mode)
|
||||
|
||||
# Hold new state temporarily
|
||||
self._held_mode = new_mode
|
||||
self._hold_end_time = time.time() + self._hold_duration
|
||||
return True
|
||||
return False
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
center_x = int(self._rect.x + self._rect.width // 2)
|
||||
center_y = int(self._rect.y + self._rect.height // 2)
|
||||
|
||||
mouse_over = rl.check_collision_point_rec(rl.get_mouse_position(), self._rect)
|
||||
mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._is_pressed
|
||||
self._white_color.a = 180 if (mouse_down and mouse_over) or not self._engageable else 255
|
||||
|
||||
texture = self._txt_exp if self._held_or_actual_mode() else self._txt_wheel
|
||||
rl.draw_circle(center_x, center_y, self._rect.width / 2, self._black_bg)
|
||||
rl.draw_texture(texture, center_x - texture.width // 2, center_y - texture.height // 2, self._white_color)
|
||||
|
||||
def _held_or_actual_mode(self):
|
||||
now = time.time()
|
||||
if self._hold_end_time and now < self._hold_end_time:
|
||||
return self._held_mode
|
||||
|
||||
if self._hold_end_time and now >= self._hold_end_time:
|
||||
self._hold_end_time = self._held_mode = None
|
||||
|
||||
return self._experimental_mode
|
||||
|
||||
def _is_toggle_allowed(self):
|
||||
if not self._params.get_bool("ExperimentalModeConfirmed"):
|
||||
return False
|
||||
|
||||
car_params = ui_state.sm["carParams"]
|
||||
if car_params.alphaLongitudinalAvailable:
|
||||
return self._params.get_bool("AlphaLongitudinalEnabled")
|
||||
else:
|
||||
return car_params.openpilotLongitudinalControl
|
||||
@@ -1,10 +1,11 @@
|
||||
import pyray as rl
|
||||
from dataclasses import dataclass
|
||||
from cereal.messaging import SubMaster
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.selfdrive.ui.onroad.exp_button import ExpButton
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
|
||||
# Constants
|
||||
SET_SPEED_NA = 255
|
||||
@@ -54,21 +55,25 @@ FONT_SIZES = FontSizes()
|
||||
COLORS = Colors()
|
||||
|
||||
|
||||
class HudRenderer:
|
||||
class HudRenderer(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
"""Initialize the HUD renderer."""
|
||||
self.is_cruise_set: bool = False
|
||||
self.is_cruise_available: bool = False
|
||||
self.set_speed: float = SET_SPEED_NA
|
||||
self.speed: float = 0.0
|
||||
self.v_ego_cluster_seen: bool = False
|
||||
self._wheel_texture: rl.Texture = gui_app.texture('icons/chffr_wheel.png', UI_CONFIG.wheel_icon_size, UI_CONFIG.wheel_icon_size)
|
||||
|
||||
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
def _update_state(self, sm: SubMaster) -> None:
|
||||
self._exp_button = ExpButton(UI_CONFIG.button_size, UI_CONFIG.wheel_icon_size)
|
||||
|
||||
def _update_state(self) -> None:
|
||||
"""Update HUD state based on car state and controls state."""
|
||||
sm = ui_state.sm
|
||||
if sm.recv_frame["carState"] < ui_state.started_frame:
|
||||
self.is_cruise_set = False
|
||||
self.set_speed = SET_SPEED_NA
|
||||
@@ -94,9 +99,9 @@ class HudRenderer:
|
||||
speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||
self.speed = max(0.0, v_ego * speed_conversion)
|
||||
|
||||
def draw(self, rect: rl.Rectangle, sm: SubMaster) -> None:
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
"""Render HUD elements to the screen."""
|
||||
self._update_state(sm)
|
||||
# Draw the header background
|
||||
rl.draw_rectangle_gradient_v(
|
||||
int(rect.x),
|
||||
int(rect.y),
|
||||
@@ -110,7 +115,13 @@ class HudRenderer:
|
||||
self._draw_set_speed(rect)
|
||||
|
||||
self._draw_current_speed(rect)
|
||||
self._draw_wheel_icon(rect)
|
||||
|
||||
button_x = rect.x + rect.width - UI_CONFIG.border_size - UI_CONFIG.button_size
|
||||
button_y = rect.y + UI_CONFIG.border_size
|
||||
self._exp_button.render(rl.Rectangle(button_x, button_y, UI_CONFIG.button_size, UI_CONFIG.button_size))
|
||||
|
||||
def handle_mouse_event(self) -> bool:
|
||||
return bool(self._exp_button.handle_mouse_event())
|
||||
|
||||
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
|
||||
"""Draw the MAX speed indicator box."""
|
||||
@@ -166,13 +177,3 @@ class HudRenderer:
|
||||
unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit)
|
||||
unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2)
|
||||
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.white_translucent)
|
||||
|
||||
def _draw_wheel_icon(self, rect: rl.Rectangle) -> None:
|
||||
"""Draw the steering wheel icon with status-based opacity."""
|
||||
center_x = int(rect.x + rect.width - UI_CONFIG.border_size - UI_CONFIG.button_size / 2)
|
||||
center_y = int(rect.y + UI_CONFIG.border_size + UI_CONFIG.button_size / 2)
|
||||
rl.draw_circle(center_x, center_y, UI_CONFIG.button_size / 2, COLORS.black_translucent)
|
||||
|
||||
opacity = 0.7 if 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.system.ui.lib.application import DEFAULT_FPS
|
||||
from openpilot.system.ui.lib.shader_polygon import draw_polygon
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT
|
||||
|
||||
|
||||
CLIP_MARGIN = 500
|
||||
MIN_DRAW_DISTANCE = 10.0
|
||||
MAX_DRAW_DISTANCE = 100.0
|
||||
@@ -36,6 +36,7 @@ class ModelPoints:
|
||||
raw_points: np.ndarray = field(default_factory=lambda: np.empty((0, 3), dtype=np.float32))
|
||||
projected_points: np.ndarray = field(default_factory=lambda: np.empty((0, 2), dtype=np.float32))
|
||||
|
||||
|
||||
@dataclass
|
||||
class LeadVehicle:
|
||||
glow: list[float] = field(default_factory=list)
|
||||
@@ -43,8 +44,9 @@ class LeadVehicle:
|
||||
fill_alpha: int = 0
|
||||
|
||||
|
||||
class ModelRenderer:
|
||||
class ModelRenderer(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._longitudinal_control = False
|
||||
self._experimental_mode = False
|
||||
self._blend_factor = 1.0
|
||||
@@ -64,11 +66,6 @@ class ModelRenderer:
|
||||
self._car_space_transform = np.zeros((3, 3), dtype=np.float32)
|
||||
self._transform_dirty = True
|
||||
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 = {
|
||||
'start': (0.0, 1.0), # Bottom of path
|
||||
@@ -86,14 +83,15 @@ class ModelRenderer:
|
||||
self._car_space_transform = transform.astype(np.float32)
|
||||
self._transform_dirty = True
|
||||
|
||||
def draw(self, rect: rl.Rectangle, sm: messaging.SubMaster):
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
sm = ui_state.sm
|
||||
|
||||
# Check if data is up-to-date
|
||||
if (sm.recv_frame["liveCalibration"] < ui_state.started_frame or
|
||||
sm.recv_frame["modelV2"] < ui_state.started_frame):
|
||||
return
|
||||
|
||||
# Set up clipping region
|
||||
self._rect = rect
|
||||
self._clip_region = rl.Rectangle(
|
||||
rect.x - CLIP_MARGIN, rect.y - CLIP_MARGIN, rect.width + 2 * CLIP_MARGIN, rect.height + 2 * CLIP_MARGIN
|
||||
)
|
||||
@@ -127,7 +125,6 @@ class ModelRenderer:
|
||||
self._update_leads(radar_state, path_x_array)
|
||||
self._transform_dirty = False
|
||||
|
||||
|
||||
# Draw elements
|
||||
self._draw_lane_lines()
|
||||
self._draw_path(sm)
|
||||
@@ -256,7 +253,7 @@ class ModelRenderer:
|
||||
glow = [(x + (sz * 1.35) + g_xo, y + sz + g_yo), (x, y - g_yo), (x - (sz * 1.35) - g_xo, y + sz + g_yo)]
|
||||
chevron = [(x + (sz * 1.25), y + sz), (x, y), (x - (sz * 1.25), y + sz)]
|
||||
|
||||
return LeadVehicle(glow=glow,chevron=chevron, fill_alpha=int(fill_alpha))
|
||||
return LeadVehicle(glow=glow, chevron=chevron, fill_alpha=int(fill_alpha))
|
||||
|
||||
def _draw_lane_lines(self):
|
||||
"""Draw lane lines and road edges"""
|
||||
@@ -357,54 +354,60 @@ class ModelRenderer:
|
||||
if points.shape[0] == 0:
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
|
||||
# Create left and right 3D points in one array
|
||||
n_points = points.shape[0]
|
||||
points_3d = self._temp_points_3d[:n_points * 2]
|
||||
points_3d[:n_points, 0] = points_3d[n_points:, 0] = points[:, 0]
|
||||
points_3d[:n_points, 1] = points[:, 1] - y_off
|
||||
points_3d[n_points:, 1] = points[:, 1] + y_off
|
||||
points_3d[:n_points, 2] = points_3d[n_points:, 2] = points[:, 2] + z_off
|
||||
N = points.shape[0]
|
||||
# Generate left and right 3D points in one array using broadcasting
|
||||
offsets = np.array([[0, -y_off, z_off], [0, y_off, z_off]], dtype=np.float32)
|
||||
points_3d = points[None, :, :] + offsets[:, None, :] # Shape: 2xNx3
|
||||
points_3d = points_3d.reshape(2 * N, 3) # Shape: (2*N)x3
|
||||
|
||||
# Single matrix multiplication for projections
|
||||
proj = np.ascontiguousarray(self._temp_proj[:, :n_points * 2]) # Slice the pre-allocated array
|
||||
np.dot(self._car_space_transform, points_3d.T, out=proj)
|
||||
valid_z = np.abs(proj[2]) > 1e-6
|
||||
if not np.any(valid_z):
|
||||
# Transform all points to projected space in one operation
|
||||
proj = self._car_space_transform @ points_3d.T # Shape: 3x(2*N)
|
||||
proj = proj.reshape(3, 2, N)
|
||||
left_proj = proj[:, 0, :]
|
||||
right_proj = proj[:, 1, :]
|
||||
|
||||
# Filter points where z is sufficiently large
|
||||
valid_proj = (np.abs(left_proj[2]) >= 1e-6) & (np.abs(right_proj[2]) >= 1e-6)
|
||||
if not np.any(valid_proj):
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
|
||||
# Compute screen coordinates
|
||||
screen = proj[:2, valid_z] / proj[2, valid_z][None, :]
|
||||
left_screen = screen[:, :n_points].T
|
||||
right_screen = screen[:, n_points:].T
|
||||
left_screen = left_proj[:2, valid_proj] / left_proj[2, valid_proj][None, :]
|
||||
right_screen = right_proj[:2, valid_proj] / right_proj[2, valid_proj][None, :]
|
||||
|
||||
# Ensure consistent shapes by re-aligning valid points
|
||||
valid_points = np.minimum(left_screen.shape[0], right_screen.shape[0])
|
||||
if valid_points == 0:
|
||||
# Define clip region bounds
|
||||
clip = self._clip_region
|
||||
x_min, x_max = clip.x, clip.x + clip.width
|
||||
y_min, y_max = clip.y, clip.y + clip.height
|
||||
|
||||
# Filter points within clip region
|
||||
left_in_clip = (
|
||||
(left_screen[0] >= x_min) & (left_screen[0] <= x_max) &
|
||||
(left_screen[1] >= y_min) & (left_screen[1] <= y_max)
|
||||
)
|
||||
right_in_clip = (
|
||||
(right_screen[0] >= x_min) & (right_screen[0] <= x_max) &
|
||||
(right_screen[1] >= y_min) & (right_screen[1] <= y_max)
|
||||
)
|
||||
both_in_clip = left_in_clip & right_in_clip
|
||||
|
||||
if not np.any(both_in_clip):
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
left_screen = left_screen[:valid_points]
|
||||
right_screen = right_screen[:valid_points]
|
||||
|
||||
if self._clip_region:
|
||||
clip = self._clip_region
|
||||
bounds_mask = (
|
||||
(left_screen[:, 0] >= clip.x) & (left_screen[:, 0] <= clip.x + clip.width) &
|
||||
(left_screen[:, 1] >= clip.y) & (left_screen[:, 1] <= clip.y + clip.height) &
|
||||
(right_screen[:, 0] >= clip.x) & (right_screen[:, 0] <= clip.x + clip.width) &
|
||||
(right_screen[:, 1] >= clip.y) & (right_screen[:, 1] <= clip.y + clip.height)
|
||||
)
|
||||
if not np.any(bounds_mask):
|
||||
# Select valid and clipped points
|
||||
left_screen = left_screen[:, both_in_clip]
|
||||
right_screen = right_screen[:, both_in_clip]
|
||||
|
||||
# Handle Y-coordinate inversion on hills
|
||||
if not allow_invert and left_screen.shape[1] > 1:
|
||||
y = left_screen[1, :] # y-coordinates
|
||||
keep = y == np.minimum.accumulate(y)
|
||||
if not np.any(keep):
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
left_screen = left_screen[bounds_mask]
|
||||
right_screen = right_screen[bounds_mask]
|
||||
left_screen = left_screen[:, keep]
|
||||
right_screen = right_screen[:, keep]
|
||||
|
||||
if not allow_invert and left_screen.shape[0] > 1:
|
||||
keep = np.concatenate(([True], np.diff(left_screen[:, 1]) < 0))
|
||||
left_screen = left_screen[keep]
|
||||
right_screen = right_screen[keep]
|
||||
if left_screen.shape[0] == 0:
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
|
||||
return np.vstack((left_screen, right_screen[::-1])).astype(np.float32)
|
||||
return np.vstack((left_screen.T, right_screen[:, ::-1].T)).astype(np.float32)
|
||||
|
||||
@staticmethod
|
||||
def _map_val(x, x0, x1, y0, y1):
|
||||
@@ -417,10 +420,10 @@ class ModelRenderer:
|
||||
def _hsla_to_color(h, s, l, a):
|
||||
rgb = colorsys.hls_to_rgb(h, l, s)
|
||||
return rl.Color(
|
||||
int(rgb[0] * 255),
|
||||
int(rgb[1] * 255),
|
||||
int(rgb[2] * 255),
|
||||
int(a * 255)
|
||||
int(rgb[0] * 255),
|
||||
int(rgb[1] * 255),
|
||||
int(rgb[2] * 255),
|
||||
int(a * 255)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -22,7 +22,7 @@ FirehosePanel::FirehosePanel(SettingsWindow *parent) : QWidget((QWidget*)parent)
|
||||
layout->setSpacing(20);
|
||||
|
||||
// header
|
||||
QLabel *title = new QLabel(tr("🔥 Firehose Mode 🔥"));
|
||||
QLabel *title = new QLabel(tr("Firehose Mode"));
|
||||
title->setStyleSheet("font-size: 100px; font-weight: 500; font-family: 'Noto Color Emoji';");
|
||||
layout->addWidget(title, 0, Qt::AlignCenter);
|
||||
|
||||
|
||||
@@ -139,6 +139,15 @@ void TogglesPanel::expandToggleDescription(const QString ¶m) {
|
||||
toggles[param.toStdString()]->showDescription();
|
||||
}
|
||||
|
||||
void TogglesPanel::scrollToToggle(const QString ¶m) {
|
||||
if (auto it = toggles.find(param.toStdString()); it != toggles.end()) {
|
||||
auto scroll_area = qobject_cast<QScrollArea*>(parent()->parent());
|
||||
if (scroll_area) {
|
||||
scroll_area->ensureWidgetVisible(it->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TogglesPanel::showEvent(QShowEvent *event) {
|
||||
updateToggles();
|
||||
}
|
||||
@@ -222,7 +231,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
addItem(dcamBtn);
|
||||
#endif
|
||||
|
||||
auto resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), "");
|
||||
resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), "");
|
||||
connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription);
|
||||
connect(resetCalibBtn, &ButtonControl::clicked, [&]() {
|
||||
if (!uiState()->engaged()) {
|
||||
@@ -235,6 +244,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
params.remove("LiveParametersV2");
|
||||
params.remove("LiveDelay");
|
||||
params.putBool("OnroadCycleRequested", true);
|
||||
updateCalibDescription();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -313,9 +323,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
}
|
||||
|
||||
void DevicePanel::updateCalibDescription() {
|
||||
QString desc =
|
||||
tr("sunnypilot requires the device to be mounted within 4° left or right and "
|
||||
"within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required.");
|
||||
QString desc = tr("sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down.");
|
||||
std::string calib_bytes = params.get("CalibrationParams");
|
||||
if (!calib_bytes.empty()) {
|
||||
try {
|
||||
@@ -333,8 +341,53 @@ void DevicePanel::updateCalibDescription() {
|
||||
qInfo() << "invalid CalibrationParams";
|
||||
}
|
||||
}
|
||||
desc += tr(" Resetting calibration will restart openpilot if the car is powered on.");
|
||||
qobject_cast<ButtonControl *>(sender())->setDescription(desc);
|
||||
|
||||
const bool is_release = params.getBool("IsReleaseBranch");
|
||||
if (!is_release) {
|
||||
int lag_perc = 0;
|
||||
std::string lag_bytes = params.get("LiveDelay");
|
||||
if (!lag_bytes.empty()) {
|
||||
try {
|
||||
AlignedBuffer aligned_buf;
|
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(lag_bytes.data(), lag_bytes.size()));
|
||||
lag_perc = cmsg.getRoot<cereal::Event>().getLiveDelay().getCalPerc();
|
||||
} catch (kj::Exception) {
|
||||
qInfo() << "invalid LiveDelay";
|
||||
}
|
||||
}
|
||||
desc += "\n\n";
|
||||
if (lag_perc < 100) {
|
||||
desc += tr("Steering lag calibration is %1% complete.").arg(lag_perc);
|
||||
} else {
|
||||
desc += tr("Steering lag calibration is complete.");
|
||||
}
|
||||
}
|
||||
|
||||
std::string torque_bytes = params.get("LiveTorqueParameters");
|
||||
if (!torque_bytes.empty()) {
|
||||
try {
|
||||
AlignedBuffer aligned_buf;
|
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(torque_bytes.data(), torque_bytes.size()));
|
||||
auto torque = cmsg.getRoot<cereal::Event>().getLiveTorqueParameters();
|
||||
// don't add for non-torque cars
|
||||
if (torque.getUseParams()) {
|
||||
int torque_perc = torque.getCalPerc();
|
||||
desc += is_release ? "\n\n" : " ";
|
||||
if (torque_perc < 100) {
|
||||
desc += tr("Steering torque response calibration is %1% complete.").arg(torque_perc);
|
||||
} else {
|
||||
desc += tr("Steering torque response calibration is complete.");
|
||||
}
|
||||
}
|
||||
} catch (kj::Exception) {
|
||||
qInfo() << "invalid LiveTorqueParameters";
|
||||
}
|
||||
}
|
||||
|
||||
desc += "\n\n";
|
||||
desc += tr("openpilot is continuously calibrating, resetting is rarely required. "
|
||||
"Resetting calibration will restart openpilot if the car is powered on.");
|
||||
resetCalibBtn->setDescription(desc);
|
||||
}
|
||||
|
||||
void DevicePanel::reboot() {
|
||||
@@ -387,6 +440,7 @@ void SettingsWindow::setCurrentPanel(int index, const QString ¶m) {
|
||||
}
|
||||
} else {
|
||||
emit expandToggleDescription(param);
|
||||
emit scrollToToggle(param);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,6 +481,7 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) {
|
||||
|
||||
TogglesPanel *toggles = new TogglesPanel(this);
|
||||
QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription);
|
||||
QObject::connect(this, &SettingsWindow::scrollToToggle, toggles, &TogglesPanel::scrollToToggle);
|
||||
|
||||
auto networking = new Networking(this);
|
||||
QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType);
|
||||
|
||||
@@ -42,6 +42,7 @@ signals:
|
||||
void reviewTrainingGuide();
|
||||
void showDriverView();
|
||||
void expandToggleDescription(const QString ¶m);
|
||||
void scrollToToggle(const QString ¶m);
|
||||
|
||||
protected:
|
||||
QPushButton *sidebar_alert_widget;
|
||||
@@ -67,6 +68,7 @@ protected slots:
|
||||
protected:
|
||||
Params params;
|
||||
ButtonControl *pair_device;
|
||||
ButtonControl *resetCalibBtn;
|
||||
};
|
||||
|
||||
class TogglesPanel : public ListWidget {
|
||||
@@ -77,6 +79,7 @@ public:
|
||||
|
||||
public slots:
|
||||
void expandToggleDescription(const QString ¶m);
|
||||
void scrollToToggle(const QString ¶m);
|
||||
|
||||
protected slots:
|
||||
virtual void updateState(const UIState &s);
|
||||
|
||||
@@ -136,6 +136,59 @@ QWidget * Setup::low_voltage() {
|
||||
return widget;
|
||||
}
|
||||
|
||||
QWidget * Setup::custom_software_warning() {
|
||||
QWidget *widget = new QWidget();
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(widget);
|
||||
main_layout->setContentsMargins(55, 0, 55, 55);
|
||||
main_layout->setSpacing(0);
|
||||
|
||||
QVBoxLayout *inner_layout = new QVBoxLayout();
|
||||
inner_layout->setContentsMargins(110, 110, 300, 0);
|
||||
main_layout->addLayout(inner_layout);
|
||||
|
||||
QLabel *title = new QLabel(tr("WARNING: Custom Software"));
|
||||
title->setStyleSheet("font-size: 90px; font-weight: 500; color: #FF594F;");
|
||||
inner_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft);
|
||||
|
||||
inner_layout->addSpacing(25);
|
||||
|
||||
QLabel *body = new QLabel(tr("Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.\n\nIf you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later."));
|
||||
body->setWordWrap(true);
|
||||
body->setAlignment(Qt::AlignTop | Qt::AlignLeft);
|
||||
body->setStyleSheet("font-size: 65px; font-weight: 300;");
|
||||
inner_layout->addWidget(body);
|
||||
|
||||
inner_layout->addStretch();
|
||||
|
||||
QHBoxLayout *blayout = new QHBoxLayout();
|
||||
blayout->setSpacing(50);
|
||||
main_layout->addLayout(blayout, 0);
|
||||
|
||||
QPushButton *back = new QPushButton(tr("Back"));
|
||||
back->setObjectName("navBtn");
|
||||
blayout->addWidget(back);
|
||||
QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage);
|
||||
|
||||
QPushButton *cont = new QPushButton(tr("Continue"));
|
||||
cont->setObjectName("navBtn");
|
||||
blayout->addWidget(cont);
|
||||
QObject::connect(cont, &QPushButton::clicked, this, [=]() {
|
||||
QTimer::singleShot(0, [=]() {
|
||||
setCurrentWidget(downloading_widget);
|
||||
});
|
||||
QString url = InputDialog::getText(tr("Enter URL"), this, tr("for Custom Software"));
|
||||
if (!url.isEmpty()) {
|
||||
QTimer::singleShot(1000, this, [=]() {
|
||||
download(url);
|
||||
});
|
||||
} else {
|
||||
setCurrentWidget(software_selection_widget);
|
||||
}
|
||||
});
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
QWidget * Setup::getting_started() {
|
||||
QWidget *widget = new QWidget();
|
||||
|
||||
@@ -305,20 +358,17 @@ QWidget * Setup::software_selection() {
|
||||
blayout->addWidget(cont);
|
||||
|
||||
QObject::connect(cont, &QPushButton::clicked, [=]() {
|
||||
auto w = currentWidget();
|
||||
QTimer::singleShot(0, [=]() {
|
||||
setCurrentWidget(downloading_widget);
|
||||
});
|
||||
QString url = OPENPILOT_URL;
|
||||
if (group->checkedButton() != openpilot) {
|
||||
url = InputDialog::getText(tr("Enter URL"), this, tr("for Custom Software"));
|
||||
}
|
||||
if (!url.isEmpty()) {
|
||||
QTimer::singleShot(1000, this, [=]() {
|
||||
download(url);
|
||||
QTimer::singleShot(0, [=]() {
|
||||
setCurrentWidget(custom_software_warning_widget);
|
||||
});
|
||||
} else {
|
||||
setCurrentWidget(w);
|
||||
QTimer::singleShot(0, [=]() {
|
||||
setCurrentWidget(downloading_widget);
|
||||
});
|
||||
QTimer::singleShot(1000, this, [=]() {
|
||||
download(OPENPILOT_URL);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -415,8 +465,10 @@ Setup::Setup(QWidget *parent) : QStackedWidget(parent) {
|
||||
|
||||
addWidget(getting_started());
|
||||
addWidget(network_setup());
|
||||
addWidget(software_selection());
|
||||
|
||||
software_selection_widget = software_selection();
|
||||
addWidget(software_selection_widget);
|
||||
custom_software_warning_widget = custom_software_warning();
|
||||
addWidget(custom_software_warning_widget);
|
||||
downloading_widget = downloading();
|
||||
addWidget(downloading_widget);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ public:
|
||||
private:
|
||||
void selectLanguage();
|
||||
QWidget *low_voltage();
|
||||
QWidget *custom_software_warning();
|
||||
QWidget *getting_started();
|
||||
QWidget *network_setup();
|
||||
QWidget *software_selection();
|
||||
@@ -23,6 +24,8 @@ private:
|
||||
|
||||
QWidget *failed_widget;
|
||||
QWidget *downloading_widget;
|
||||
QWidget *custom_software_warning_widget;
|
||||
QWidget *software_selection_widget;
|
||||
QTranslator translator;
|
||||
|
||||
signals:
|
||||
|
||||
@@ -49,6 +49,10 @@ lateral_panel_qt_src = [
|
||||
"sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc",
|
||||
]
|
||||
|
||||
longitudinal_panel_qt_src = [
|
||||
"sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.cc",
|
||||
]
|
||||
|
||||
network_src = [
|
||||
"sunnypilot/qt/network/sunnylink/sunnylink_client.cc",
|
||||
"sunnypilot/qt/network/sunnylink/services/base_device_service.cc",
|
||||
@@ -83,7 +87,7 @@ brand_settings_qt_src = [
|
||||
|
||||
|
||||
sp_widgets_src = widgets_src + network_src
|
||||
sp_qt_src = qt_src + lateral_panel_qt_src + vehicle_panel_qt_src + brand_settings_qt_src + osm_panel_qt_src
|
||||
sp_qt_src = qt_src + lateral_panel_qt_src + vehicle_panel_qt_src + brand_settings_qt_src + longitudinal_panel_qt_src + osm_panel_qt_src
|
||||
sp_qt_util = qt_util
|
||||
|
||||
Export('sp_widgets_src', 'sp_qt_src', "sp_qt_util")
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.h"
|
||||
|
||||
CustomAccIncrement::CustomAccIncrement(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent)
|
||||
: ExpandableToggleRow(param, title, desc, icon, parent) {
|
||||
auto *accFrame = new QFrame(this);
|
||||
auto *accFrameLayout = new QGridLayout();
|
||||
accFrame->setLayout(accFrameLayout);
|
||||
accFrameLayout->setSpacing(0);
|
||||
|
||||
auto *shortPressControl = new AccIncrementOptionControl("CustomAccShortPressIncrement", {1, 10}, 1);
|
||||
connect(shortPressControl, &OptionControlSP::updateLabels, shortPressControl, &AccIncrementOptionControl::refresh);
|
||||
|
||||
auto *longPressControl = new AccIncrementOptionControl("CustomAccLongPressIncrement", {1, 3}, 1, &customLongValues);
|
||||
connect(longPressControl, &OptionControlSP::updateLabels, longPressControl, &AccIncrementOptionControl::refresh);
|
||||
|
||||
shortPressControl->setFixedWidth(280);
|
||||
longPressControl->setFixedWidth(280);
|
||||
accFrameLayout->addWidget(shortPressControl, 0, 0, Qt::AlignLeft);
|
||||
accFrameLayout->addWidget(longPressControl, 0, 1, Qt::AlignRight);
|
||||
|
||||
addItem(accFrame);
|
||||
}
|
||||
|
||||
AccIncrementOptionControl::AccIncrementOptionControl(const QString ¶m, const MinMaxValue &range, const int per_value_change, const QMap<QString, QString> *valMap)
|
||||
: OptionControlSP(param, "", "", "", range, per_value_change, true, valMap) {
|
||||
param_name = param.toStdString();
|
||||
refresh();
|
||||
}
|
||||
|
||||
void AccIncrementOptionControl::refresh() {
|
||||
std::string val = params.get(param_name);
|
||||
std::string label = "<span style='font-size: 45px; font-weight: 450; color: #FFFFFF;'>";
|
||||
label += param_name == "CustomAccShortPressIncrement" ? "Short Press" : "Long Press";
|
||||
label += " <br><span style='font-size: 40px; font-weight: 450; color:rgb(174, 255, 195);'>" + val;
|
||||
label += param_name == "CustomAccShortPressIncrement"
|
||||
? (val == "1" ? " (Default)" : "")
|
||||
: (val == "5" ? " (Default)" : "");
|
||||
label += "</span></span>";
|
||||
setLabel(QString::fromStdString(label));
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/expandable_row.h"
|
||||
|
||||
class CustomAccIncrement : public ExpandableToggleRow {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CustomAccIncrement(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr);
|
||||
|
||||
private:
|
||||
QMap<QString, QString> customLongValues = {
|
||||
{"1", "1"},
|
||||
{"2", "5"}, // Default
|
||||
{"3", "10"}
|
||||
};
|
||||
};
|
||||
|
||||
class AccIncrementOptionControl : public OptionControlSP {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AccIncrementOptionControl(const QString ¶m, const MinMaxValue &range, int per_value_change, const QMap<QString, QString> *valMap = nullptr);
|
||||
void refresh();
|
||||
|
||||
protected:
|
||||
std::string param_name;
|
||||
|
||||
private:
|
||||
Params params;
|
||||
};
|
||||
@@ -8,4 +8,72 @@
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h"
|
||||
|
||||
LongitudinalPanel::LongitudinalPanel(QWidget *parent) : QWidget(parent) {
|
||||
main_layout = new QStackedLayout(this);
|
||||
ListWidget *list = new ListWidget(this, false);
|
||||
|
||||
cruisePanelScreen = new QWidget(this);
|
||||
QVBoxLayout *vlayout = new QVBoxLayout(cruisePanelScreen);
|
||||
vlayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
cruisePanelScroller = new ScrollViewSP(list, this);
|
||||
vlayout->addWidget(cruisePanelScroller);
|
||||
|
||||
customAccIncrement = new CustomAccIncrement("CustomAccIncrementsEnabled", tr("Custom ACC Speed Increments"), "", "", this);
|
||||
list->addItem(customAccIncrement);
|
||||
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, this, &LongitudinalPanel::refresh);
|
||||
|
||||
main_layout->addWidget(cruisePanelScreen);
|
||||
main_layout->setCurrentWidget(cruisePanelScreen);
|
||||
refresh(offroad);
|
||||
}
|
||||
|
||||
void LongitudinalPanel::showEvent(QShowEvent *event) {
|
||||
main_layout->setCurrentWidget(cruisePanelScreen);
|
||||
refresh(offroad);
|
||||
}
|
||||
|
||||
void LongitudinalPanel::refresh(bool _offroad) {
|
||||
auto cp_bytes = params.get("CarParamsPersistent");
|
||||
if (!cp_bytes.empty()) {
|
||||
AlignedBuffer aligned_buf;
|
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size()));
|
||||
cereal::CarParams::Reader CP = cmsg.getRoot<cereal::CarParams>();
|
||||
|
||||
has_longitudinal_control = hasLongitudinalControl(CP);
|
||||
is_pcm_cruise = CP.getPcmCruise();
|
||||
} else {
|
||||
has_longitudinal_control = false;
|
||||
is_pcm_cruise = false;
|
||||
}
|
||||
|
||||
QString accEnabledDescription = tr("Enable custom Short & Long press increments for cruise speed increase/decrease.");
|
||||
QString accNoLongDescription = tr("This feature can only be used with openpilot longitudinal control enabled.");
|
||||
QString accPcmCruiseDisabledDescription = tr("This feature is not supported on this platform due to vehicle limitations.");
|
||||
QString onroadOnlyDescription = tr("Start the vehicle to check vehicle compatibility.");
|
||||
|
||||
if (offroad) {
|
||||
customAccIncrement->setDescription(onroadOnlyDescription);
|
||||
customAccIncrement->showDescription();
|
||||
} else {
|
||||
if (has_longitudinal_control) {
|
||||
if (is_pcm_cruise) {
|
||||
customAccIncrement->setDescription(accPcmCruiseDisabledDescription);
|
||||
customAccIncrement->showDescription();
|
||||
} else {
|
||||
customAccIncrement->setDescription(accEnabledDescription);
|
||||
}
|
||||
} else {
|
||||
params.remove("CustomAccIncrementsEnabled");
|
||||
customAccIncrement->toggleFlipped(false);
|
||||
customAccIncrement->setDescription(accNoLongDescription);
|
||||
customAccIncrement->showDescription();
|
||||
}
|
||||
}
|
||||
|
||||
// enable toggle when long is available and is not PCM cruise
|
||||
customAccIncrement->setEnabled(has_longitudinal_control && !is_pcm_cruise && !offroad);
|
||||
customAccIncrement->refresh();
|
||||
|
||||
offroad = _offroad;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,26 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
|
||||
|
||||
class LongitudinalPanel : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LongitudinalPanel(QWidget *parent = nullptr);
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void refresh(bool _offroad);
|
||||
|
||||
private:
|
||||
Params params;
|
||||
bool has_longitudinal_control = false;
|
||||
bool is_pcm_cruise = false;
|
||||
bool offroad = false;
|
||||
|
||||
QStackedLayout *main_layout = nullptr;
|
||||
ScrollViewSP *cruisePanelScroller = nullptr;
|
||||
QWidget *cruisePanelScreen = nullptr;
|
||||
CustomAccIncrement *customAccIncrement = nullptr;
|
||||
};
|
||||
|
||||
@@ -7,21 +7,55 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <QJsonDocument>
|
||||
#include <QStyle>
|
||||
|
||||
#include "common/model.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
|
||||
|
||||
static const QString progressStyleActive = "QProgressBar {"
|
||||
" font-size: 40px;"
|
||||
" font-weight: 200;"
|
||||
" padding: 1px;"
|
||||
" border: 3px solid black;"
|
||||
" border-radius: 10px;"
|
||||
"}"
|
||||
"QProgressBar::chunk {"
|
||||
" background-color: #1e79e8;"
|
||||
" border-radius: 10px;"
|
||||
"}";
|
||||
|
||||
static const QString progressStyleInactive = progressStyleActive +
|
||||
"QProgressBar::chunk {"
|
||||
" background-color: transparent;"
|
||||
"}";
|
||||
|
||||
static const QString progressStyleDone = progressStyleActive +
|
||||
"QProgressBar {"
|
||||
" color: #33ab4c;"
|
||||
"}"
|
||||
"QProgressBar::chunk {"
|
||||
" background-color: transparent;"
|
||||
"}";
|
||||
|
||||
static const QString progressStyleError = progressStyleActive +
|
||||
"QProgressBar {"
|
||||
" color: red;"
|
||||
"}"
|
||||
"QProgressBar::chunk {"
|
||||
" background-color: transparent;"
|
||||
"}";
|
||||
|
||||
ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
main_layout->setContentsMargins(50, 20, 50, 20);
|
||||
|
||||
ListWidgetSP *list = new ListWidgetSP(this);
|
||||
ListWidgetSP *list = new ListWidgetSP(this, false);
|
||||
ScrollViewSP *scroller = new ScrollViewSP(list, this);
|
||||
main_layout->addWidget(scroller);
|
||||
|
||||
const auto current_model = GetActiveModelName();
|
||||
currentModelLblBtn = new ButtonControlSP(tr("Current Model"), tr("SELECT"), current_model);
|
||||
currentModelLblBtn = new ButtonControlSP(tr("Current Model"), tr("SELECT"), "", this);
|
||||
currentModelLblBtn->setValue(current_model);
|
||||
|
||||
connect(currentModelLblBtn, &ButtonControlSP::clicked, this, &ModelsPanel::handleCurrentModelLblBtnClicked);
|
||||
@@ -32,6 +66,29 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) {
|
||||
connect(uiStateSP(), &UIStateSP::uiUpdate, this, &ModelsPanel::updateLabels);
|
||||
list->addItem(currentModelLblBtn);
|
||||
|
||||
// Create progress bars for downloads
|
||||
supercomboProgressBar = createProgressBar(this);
|
||||
QString supercomboType = tr("Driving Model");
|
||||
supercomboFrame = createModelDetailFrame(this, supercomboType, supercomboProgressBar);
|
||||
list->addItem(supercomboFrame);
|
||||
|
||||
navigationProgressBar = createProgressBar(this);
|
||||
QString navigationType = tr("Navigation Model");
|
||||
navigationFrame = createModelDetailFrame(this, navigationType, navigationProgressBar);
|
||||
list->addItem(navigationFrame);
|
||||
|
||||
visionProgressBar = createProgressBar(this);
|
||||
QString visionType = tr("Vision Model");
|
||||
visionFrame = createModelDetailFrame(this, visionType, visionProgressBar);
|
||||
list->addItem(visionFrame);
|
||||
|
||||
policyProgressBar = createProgressBar(this);
|
||||
QString policyType = tr("Policy Model");
|
||||
policyFrame = createModelDetailFrame(this, policyType, policyProgressBar);
|
||||
list->addItem(policyFrame);
|
||||
|
||||
list->addItem(horizontal_line());
|
||||
|
||||
// LiveDelay toggle
|
||||
list->addItem(new ParamControlSP("LagdToggle",
|
||||
tr("Live Learning Steer Delay"),
|
||||
@@ -40,15 +97,38 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) {
|
||||
"../assets/offroad/icon_shell.png"));
|
||||
}
|
||||
|
||||
QProgressBar* ModelsPanel::createProgressBar(QWidget *parent) {
|
||||
QProgressBar *progressBar = new QProgressBar(parent);
|
||||
progressBar->setRange(0, 100);
|
||||
progressBar->setValue(0);
|
||||
progressBar->setTextVisible(true);
|
||||
progressBar->setAlignment(Qt::AlignVCenter);
|
||||
return progressBar;
|
||||
}
|
||||
|
||||
QFrame* ModelsPanel::createModelDetailFrame(QWidget *parent, QString &typeName, QProgressBar *progressBar) {
|
||||
QFrame *frame = new QFrame(parent);
|
||||
QHBoxLayout *layout = new QHBoxLayout(frame);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(50);
|
||||
layout->addWidget(new QLabel(typeName));
|
||||
layout->addWidget(progressBar);
|
||||
frame->setVisible(false);
|
||||
return frame;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Updates the UI with bundle download progress information
|
||||
* Reads status from modelManagerSP cereal message and displays status for all models
|
||||
*/
|
||||
void ModelsPanel::handleBundleDownloadProgress() {
|
||||
supercomboFrame->setVisible(false);
|
||||
visionFrame->setVisible(false);
|
||||
policyFrame->setVisible(false);
|
||||
navigationFrame->setVisible(false);
|
||||
|
||||
using DS = cereal::ModelManagerSP::DownloadStatus;
|
||||
if (!model_manager.hasSelectedBundle() && !model_manager.hasActiveBundle()) {
|
||||
currentModelLblBtn->setDescription(tr("No custom model selected!"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -61,21 +141,27 @@ void ModelsPanel::handleBundleDownloadProgress() {
|
||||
|
||||
// Get status for each model type in order
|
||||
for (const auto &model: models) {
|
||||
QString typeName;
|
||||
QString modelName = QString::fromStdString(bundle.getDisplayName());
|
||||
|
||||
QProgressBar *progressBar = nullptr;
|
||||
QFrame *modelFrame = nullptr;
|
||||
|
||||
switch (model.getType()) {
|
||||
case cereal::ModelManagerSP::Model::Type::SUPERCOMBO:
|
||||
typeName = tr("Driving");
|
||||
progressBar = supercomboProgressBar;
|
||||
modelFrame = supercomboFrame;
|
||||
break;
|
||||
case cereal::ModelManagerSP::Model::Type::NAVIGATION:
|
||||
typeName = tr("Navigation");
|
||||
progressBar = navigationProgressBar;
|
||||
modelFrame = navigationFrame;
|
||||
break;
|
||||
case cereal::ModelManagerSP::Model::Type::VISION:
|
||||
typeName = tr("Vision");
|
||||
progressBar = visionProgressBar;
|
||||
modelFrame = visionFrame;
|
||||
break;
|
||||
case cereal::ModelManagerSP::Model::Type::POLICY:
|
||||
typeName = tr("Policy");
|
||||
progressBar = policyProgressBar;
|
||||
modelFrame = policyFrame;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -83,31 +169,26 @@ void ModelsPanel::handleBundleDownloadProgress() {
|
||||
QString line;
|
||||
|
||||
if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING) {
|
||||
line = tr("Downloading %1 model [%2]... (%3%)").arg(typeName, modelName).arg(progress.getProgress(), 0, 'f', 2);
|
||||
progressBar->setStyleSheet(progressStyleActive);
|
||||
progressBar->setValue(progress.getProgress());
|
||||
progressBar->setFormat(QString(" %1% - %2").arg(static_cast<int>(progress.getProgress())).arg(modelName));
|
||||
device()->resetInteractiveTimeout();
|
||||
} else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADED) {
|
||||
line = tr("%1 model [%2] %3").arg(typeName, modelName, download_status_changed ? tr("downloaded") : tr("ready"));
|
||||
progressBar->setStyleSheet(progressStyleDone);
|
||||
progressBar->setFormat(tr(" %1 - %2").arg(modelName, download_status_changed ? tr("downloaded") : tr("ready")));
|
||||
} else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::CACHED) {
|
||||
line = tr("%1 model [%2] %3").arg(typeName, modelName, download_status_changed ? tr("from cache") : tr("ready"));
|
||||
progressBar->setStyleSheet(progressStyleDone);
|
||||
progressBar->setFormat(tr(" %1 - %2").arg(modelName, download_status_changed ? tr("from cache") : tr("ready")));
|
||||
} else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::FAILED) {
|
||||
line = tr("%1 model [%2] download failed").arg(typeName, modelName);
|
||||
progressBar->setStyleSheet(progressStyleError);
|
||||
progressBar->setFormat(tr(" download failed - %1").arg(modelName));
|
||||
} else {
|
||||
line = tr("%1 model [%2] pending...").arg(typeName, modelName);
|
||||
progressBar->setStyleSheet(progressStyleInactive);
|
||||
progressBar->setFormat(tr(" pending - %1").arg(modelName));
|
||||
}
|
||||
status.append(line);
|
||||
}
|
||||
|
||||
currentModelLblBtn->setDescription(status.join("\n"));
|
||||
|
||||
if (prev_download_status != download_status) {
|
||||
switch (bundle.getStatus()) {
|
||||
case cereal::ModelManagerSP::DownloadStatus::DOWNLOADING:
|
||||
case cereal::ModelManagerSP::DownloadStatus::CACHED:
|
||||
case cereal::ModelManagerSP::DownloadStatus::DOWNLOADED:
|
||||
currentModelLblBtn->showDescription();
|
||||
break;
|
||||
case cereal::ModelManagerSP::DownloadStatus::FAILED:
|
||||
default:
|
||||
break;
|
||||
// keep navigation hidden for now to avoid confusion
|
||||
if (model.getType() != cereal::ModelManagerSP::Model::Type::NAVIGATION) {
|
||||
modelFrame->setVisible(true);
|
||||
}
|
||||
}
|
||||
prev_download_status = download_status;
|
||||
@@ -125,6 +206,17 @@ QString ModelsPanel::GetActiveModelName() {
|
||||
return DEFAULT_MODEL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gets the short name of the currently selected model bundle
|
||||
* @return Display short name of the selected bundle or default model name
|
||||
*/
|
||||
QString ModelsPanel::GetActiveModelInternalName() {
|
||||
if (model_manager.hasActiveBundle()) {
|
||||
return QString::fromStdString(model_manager.getActiveBundle().getInternalName());
|
||||
}
|
||||
return DEFAULT_MODEL;
|
||||
}
|
||||
|
||||
void ModelsPanel::updateModelManagerState() {
|
||||
const SubMaster &sm = *(uiStateSP()->sm);
|
||||
model_manager = sm["modelManagerSP"].getModelManagerSP();
|
||||
@@ -156,7 +248,7 @@ void ModelsPanel::handleCurrentModelLblBtnClicked() {
|
||||
bundleNames.append(index_to_bundle[index]);
|
||||
}
|
||||
|
||||
currentModelLblBtn->setValue(GetActiveModelName());
|
||||
currentModelLblBtn->setValue(GetActiveModelInternalName());
|
||||
|
||||
const QString selectedBundleName = MultiOptionDialog::getSelection(
|
||||
tr("Select a Model"), bundleNames, GetActiveModelName(), this);
|
||||
@@ -197,7 +289,7 @@ void ModelsPanel::updateLabels() {
|
||||
updateModelManagerState();
|
||||
handleBundleDownloadProgress();
|
||||
currentModelLblBtn->setEnabled(!is_onroad && !isDownloading());
|
||||
currentModelLblBtn->setValue(GetActiveModelName());
|
||||
currentModelLblBtn->setValue(GetActiveModelInternalName());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QProgressBar>
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
|
||||
class ModelsPanel : public QWidget {
|
||||
@@ -17,6 +19,7 @@ public:
|
||||
|
||||
private:
|
||||
QString GetActiveModelName();
|
||||
QString GetActiveModelInternalName();
|
||||
void updateModelManagerState();
|
||||
|
||||
bool isDownloading() const {
|
||||
@@ -33,6 +36,8 @@ private:
|
||||
void handleCurrentModelLblBtnClicked();
|
||||
void handleBundleDownloadProgress();
|
||||
void showResetParamsDialog();
|
||||
QProgressBar* createProgressBar(QWidget *parent);
|
||||
QFrame* createModelDetailFrame(QWidget *parent, QString &typeName, QProgressBar *progressBar);
|
||||
cereal::ModelManagerSP::Reader model_manager;
|
||||
cereal::ModelManagerSP::DownloadStatus download_status{};
|
||||
cereal::ModelManagerSP::DownloadStatus prev_download_status{};
|
||||
@@ -59,6 +64,14 @@ private:
|
||||
bool is_onroad = false;
|
||||
|
||||
ButtonControlSP *currentModelLblBtn;
|
||||
QProgressBar *supercomboProgressBar;
|
||||
QFrame *supercomboFrame;
|
||||
QProgressBar *navigationProgressBar;
|
||||
QFrame *navigationFrame;
|
||||
QProgressBar *visionProgressBar;
|
||||
QFrame *visionFrame;
|
||||
QProgressBar *policyProgressBar;
|
||||
QFrame *policyFrame;
|
||||
Params params;
|
||||
|
||||
};
|
||||
|
||||
@@ -72,6 +72,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) {
|
||||
|
||||
TogglesPanelSP *toggles = new TogglesPanelSP(this);
|
||||
QObject::connect(this, &SettingsWindowSP::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription);
|
||||
QObject::connect(this, &SettingsWindowSP::scrollToToggle, toggles, &TogglesPanel::scrollToToggle);
|
||||
|
||||
auto networking = new NetworkingSP(this);
|
||||
QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &NetworkingSP::setPrimeType);
|
||||
|
||||
@@ -246,10 +246,10 @@ def setup_settings_steering_alc(click, pm: PubMaster, scroll=None):
|
||||
click(970, 534)
|
||||
time.sleep(UI_DELAY)
|
||||
|
||||
def setup_settings_driving(click, pm: PubMaster, scroll=None):
|
||||
def setup_settings_cruise(click, pm: PubMaster, scroll=None):
|
||||
setup_settings_device(click, pm)
|
||||
scroll(-1, 278, 962)
|
||||
click(278, 962)
|
||||
scroll(-400, 278, 962)
|
||||
click(278, 324)
|
||||
time.sleep(UI_DELAY)
|
||||
|
||||
def setup_settings_visuals(click, pm: PubMaster, scroll=None):
|
||||
@@ -312,7 +312,7 @@ CASES.update({
|
||||
"settings_steering": setup_settings_steering,
|
||||
"settings_steering_mads": setup_settings_steering_mads,
|
||||
"settings_steering_alc": setup_settings_steering_alc,
|
||||
"settings_driving": setup_settings_driving,
|
||||
"settings_cruise": setup_settings_cruise,
|
||||
"settings_visuals": setup_settings_visuals,
|
||||
"settings_trips": setup_settings_trips,
|
||||
"settings_vehicle": setup_settings_vehicle,
|
||||
@@ -391,9 +391,9 @@ def create_screenshots():
|
||||
driver_img = frames[2]
|
||||
else:
|
||||
with open(frames_cache, 'wb') as f:
|
||||
road_img = FrameReader(route.camera_paths()[segnum]).get(0, pix_fmt="nv12")[0]
|
||||
wide_road_img = FrameReader(route.ecamera_paths()[segnum]).get(0, pix_fmt="nv12")[0]
|
||||
driver_img = FrameReader(route.dcamera_paths()[segnum]).get(0, pix_fmt="nv12")[0]
|
||||
road_img = FrameReader(route.camera_paths()[segnum], pix_fmt="nv12").get(0)
|
||||
wide_road_img = FrameReader(route.ecamera_paths()[segnum], pix_fmt="nv12").get(0)
|
||||
driver_img = FrameReader(route.dcamera_paths()[segnum], pix_fmt="nv12").get(0)
|
||||
pickle.dump([road_img, wide_road_img, driver_img], f)
|
||||
|
||||
STREAMS.append((VisionStreamType.VISION_STREAM_ROAD, cam.fcam, road_img.flatten().tobytes()))
|
||||
|
||||
@@ -6,16 +6,16 @@ from openpilot.selfdrive.ui.layouts.main import MainLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
gui_app.init_window("UI")
|
||||
main_layout = MainLayout()
|
||||
main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
for _ in gui_app.render():
|
||||
ui_state.update()
|
||||
|
||||
#TODO handle brigntness and awake state here
|
||||
# TODO handle brigntness and awake state here
|
||||
|
||||
main_layout.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
main_layout.render()
|
||||
|
||||
kick_watchdog()
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import pyray as rl
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from enum import Enum
|
||||
from cereal import messaging, log
|
||||
from openpilot.common.params import Params, UnknownKeyName
|
||||
|
||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeState
|
||||
|
||||
UI_BORDER_SIZE = 30
|
||||
|
||||
@@ -44,6 +46,8 @@ class UIState:
|
||||
]
|
||||
)
|
||||
|
||||
self.prime_state = PrimeState()
|
||||
|
||||
# UI Status tracking
|
||||
self.status: UIStatus = UIStatus.DISENGAGED
|
||||
self.started_frame: int = 0
|
||||
@@ -64,10 +68,17 @@ class UIState:
|
||||
def engaged(self) -> bool:
|
||||
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:
|
||||
self.sm.update(0)
|
||||
self._update_state()
|
||||
self._update_status()
|
||||
device.update()
|
||||
|
||||
def _update_state(self) -> None:
|
||||
# Handle panda states updates
|
||||
@@ -125,5 +136,36 @@ class UIState:
|
||||
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
|
||||
ui_state = UIState()
|
||||
device = Device()
|
||||
|
||||
0
selfdrive/ui/widgets/__init__.py
Normal file
0
selfdrive/ui/widgets/__init__.py
Normal file
74
selfdrive/ui/widgets/exp_mode_button.py
Normal file
74
selfdrive/ui/widgets/exp_mode_button.py
Normal file
@@ -0,0 +1,74 @@
|
||||
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,6 +9,8 @@ from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||
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.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.widget import Widget
|
||||
|
||||
|
||||
class AlertColors:
|
||||
HIGH_SEVERITY = rl.Color(226, 44, 44, 255)
|
||||
@@ -40,8 +42,9 @@ class AlertData:
|
||||
visible: bool = False
|
||||
|
||||
|
||||
class AbstractAlert(ABC):
|
||||
class AbstractAlert(Widget, ABC):
|
||||
def __init__(self, has_reboot_btn: bool = False):
|
||||
super().__init__()
|
||||
self.params = Params()
|
||||
self.has_reboot_btn = has_reboot_btn
|
||||
self.dismiss_callback: Callable | None = None
|
||||
@@ -67,8 +70,7 @@ class AbstractAlert(ABC):
|
||||
pass
|
||||
|
||||
def handle_input(self, mouse_pos: rl.Vector2, mouse_clicked: bool) -> bool:
|
||||
# TODO: fix scroll_panel.is_click_valid()
|
||||
if not mouse_clicked:
|
||||
if not mouse_clicked or not self.scroll_panel.is_touch_valid():
|
||||
return False
|
||||
|
||||
if rl.check_collision_point_rec(mouse_pos, self.dismiss_btn_rect):
|
||||
@@ -88,7 +90,7 @@ class AbstractAlert(ABC):
|
||||
|
||||
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)
|
||||
|
||||
footer_height = AlertConstants.BUTTON_SIZE[1] + AlertConstants.SPACING
|
||||
|
||||
170
selfdrive/ui/widgets/pairing_dialog.py
Normal file
170
selfdrive/ui/widgets/pairing_dialog.py
Normal file
@@ -0,0 +1,170 @@
|
||||
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
|
||||
62
selfdrive/ui/widgets/prime.py
Normal file
62
selfdrive/ui/widgets/prime.py
Normal file
@@ -0,0 +1,62 @@
|
||||
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)
|
||||
94
selfdrive/ui/widgets/setup.py
Normal file
94
selfdrive/ui/widgets/setup.py
Normal file
@@ -0,0 +1,94 @@
|
||||
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
|
||||
128
selfdrive/ui/widgets/ssh_key.py
Normal file
128
selfdrive/ui/widgets/ssh_key.py
Normal file
@@ -0,0 +1,128 @@
|
||||
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.sunnypilot.mapd.mapd_manager import MAPD_PATH, MAPD_BIN_DIR
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from openpilot.system.ui.spinner import Spinner
|
||||
from openpilot.common.spinner import Spinner
|
||||
from openpilot.system.version import is_prebuilt
|
||||
import openpilot.system.sentry as sentry
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ common_src = [
|
||||
"transforms/transform.cc",
|
||||
]
|
||||
|
||||
|
||||
# OpenCL is a framework on Mac
|
||||
if arch == "Darwin":
|
||||
frameworks += ['OpenCL']
|
||||
@@ -29,4 +28,3 @@ for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transfor
|
||||
cython_libs = envCython["LIBS"] + libs
|
||||
commonmodel_lib = lenv.Library('commonmodel', common_src)
|
||||
lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks)
|
||||
|
||||
|
||||
@@ -118,30 +118,14 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D
|
||||
# action (includes lateral planning now)
|
||||
modelV2.action = action
|
||||
|
||||
# times at X_IDXS according to model plan
|
||||
PLAN_T_IDXS = [np.nan] * ModelConstants.IDX_N
|
||||
PLAN_T_IDXS[0] = 0.0
|
||||
plan_x = net_output_data['plan'][0,:,Plan.POSITION][:,0].tolist()
|
||||
for xidx in range(1, ModelConstants.IDX_N):
|
||||
tidx = 0
|
||||
# increment tidx until we find an element that's further away than the current xidx
|
||||
while tidx < ModelConstants.IDX_N - 1 and plan_x[tidx+1] < ModelConstants.X_IDXS[xidx]:
|
||||
tidx += 1
|
||||
if tidx == ModelConstants.IDX_N - 1:
|
||||
# if the Plan doesn't extend far enough, set plan_t to the max value (10s), then break
|
||||
PLAN_T_IDXS[xidx] = ModelConstants.T_IDXS[ModelConstants.IDX_N - 1]
|
||||
break
|
||||
# interpolate to find `t` for the current xidx
|
||||
current_x_val = plan_x[tidx]
|
||||
next_x_val = plan_x[tidx+1]
|
||||
p = (ModelConstants.X_IDXS[xidx] - current_x_val) / (next_x_val - current_x_val) if abs(next_x_val - current_x_val) > 1e-9 else float('nan')
|
||||
PLAN_T_IDXS[xidx] = p * ModelConstants.T_IDXS[tidx+1] + (1 - p) * ModelConstants.T_IDXS[tidx]
|
||||
# times at X_IDXS of edges and lines aren't used
|
||||
LINE_T_IDXS: list[float] = []
|
||||
|
||||
# lane lines
|
||||
modelV2.init('laneLines', 4)
|
||||
for i in range(4):
|
||||
lane_line = modelV2.laneLines[i]
|
||||
fill_xyzt(lane_line, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['lane_lines'][0,i,:,0], net_output_data['lane_lines'][0,i,:,1])
|
||||
fill_xyzt(lane_line, LINE_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['lane_lines'][0,i,:,0], net_output_data['lane_lines'][0,i,:,1])
|
||||
modelV2.laneLineStds = net_output_data['lane_lines_stds'][0,:,0,0].tolist()
|
||||
modelV2.laneLineProbs = net_output_data['lane_lines_prob'][0,1::2].tolist()
|
||||
|
||||
@@ -151,7 +135,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D
|
||||
modelV2.init('roadEdges', 2)
|
||||
for i in range(2):
|
||||
road_edge = modelV2.roadEdges[i]
|
||||
fill_xyzt(road_edge, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['road_edges'][0,i,:,0], net_output_data['road_edges'][0,i,:,1])
|
||||
fill_xyzt(road_edge, LINE_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['road_edges'][0,i,:,0], net_output_data['road_edges'][0,i,:,1])
|
||||
modelV2.roadEdgeStds = net_output_data['road_edges_stds'][0,:,0,0].tolist()
|
||||
|
||||
# leads
|
||||
|
||||
@@ -62,7 +62,7 @@ class ModelState:
|
||||
self.MIN_LAT_CONTROL_SPEED = 0.3
|
||||
|
||||
buffer_length = 5 if self.model_runner.is_20hz else 2
|
||||
self.frames = {'input_imgs': DrivingModelFrame(context, buffer_length), 'big_input_imgs': DrivingModelFrame(context, buffer_length)}
|
||||
self.frames = {name: DrivingModelFrame(context, buffer_length) for name in self.model_runner.vision_input_names}
|
||||
self.prev_desire = np.zeros(self.constants.DESIRE_LEN, dtype=np.float32)
|
||||
|
||||
# img buffers are managed in openCL transform code
|
||||
@@ -86,7 +86,7 @@ class ModelState:
|
||||
self.desire_reshape_dims = (self.numpy_inputs['desire'].shape[0], self.numpy_inputs['desire'].shape[1], -1,
|
||||
self.numpy_inputs['desire'].shape[2])
|
||||
|
||||
def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray,
|
||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
||||
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
|
||||
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
||||
inputs['desire'][0] = 0
|
||||
@@ -110,8 +110,7 @@ class ModelState:
|
||||
if key in inputs and key not in ['desire']:
|
||||
self.numpy_inputs[key][:] = inputs[key]
|
||||
|
||||
imgs_cl = {'input_imgs': self.frames['input_imgs'].prepare(buf, transform.flatten()),
|
||||
'big_input_imgs': self.frames['big_input_imgs'].prepare(wbuf, transform_wide.flatten())}
|
||||
imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.model_runner.vision_input_names}
|
||||
|
||||
# Prepare inputs using the model runner
|
||||
self.model_runner.prepare_inputs(imgs_cl, self.numpy_inputs, self.frames)
|
||||
@@ -315,6 +314,8 @@ def main(demo=False):
|
||||
if prepare_only:
|
||||
cloudlog.error(f"skipping model eval. Dropped {vipc_dropped_frames} frames")
|
||||
|
||||
bufs = {name: buf_extra if 'big' in name else buf_main for name in model.model_runner.vision_input_names}
|
||||
transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.model_runner.vision_input_names}
|
||||
inputs:dict[str, np.ndarray] = {
|
||||
'desire': vec_desire,
|
||||
'traffic_convention': traffic_convention,
|
||||
@@ -324,7 +325,7 @@ def main(demo=False):
|
||||
inputs['lateral_control_params'] = np.array([v_ego, steer_delay], dtype=np.float32)
|
||||
|
||||
mt1 = time.perf_counter()
|
||||
model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only)
|
||||
model_output = model.run(bufs, transforms, inputs, prepare_only)
|
||||
mt2 = time.perf_counter()
|
||||
model_execution_time = mt2 - mt1
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ from openpilot.system.hardware import PC
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from pathlib import Path
|
||||
|
||||
CURRENT_SELECTOR_VERSION = 5
|
||||
CURRENT_SELECTOR_VERSION = 6
|
||||
REQUIRED_MIN_SELECTOR_VERSION = 5
|
||||
|
||||
USE_ONNX = os.getenv('USE_ONNX', PC)
|
||||
|
||||
@@ -12,8 +12,15 @@ CUSTOM_MODEL_PATH = Paths.model_root()
|
||||
|
||||
|
||||
# Set QCOM environment variable for TICI devices, potentially enabling hardware acceleration
|
||||
if TICI:
|
||||
USBGPU = "USBGPU" in os.environ
|
||||
if USBGPU:
|
||||
os.environ['AMD'] = '1'
|
||||
os.environ['AMD_IFACE'] = 'USB'
|
||||
elif TICI:
|
||||
os.environ['QCOM'] = '1'
|
||||
else:
|
||||
os.environ['LLVM'] = '1'
|
||||
os.environ['JIT'] = '2' # TODO: This may cause issues
|
||||
|
||||
|
||||
class ModelData:
|
||||
@@ -132,6 +139,13 @@ class ModelRunner(ModularRunner):
|
||||
return self._model_data.output_slices
|
||||
raise ValueError("Model data is not available. Ensure the model is loaded correctly.")
|
||||
|
||||
@property
|
||||
def vision_input_names(self) -> list[str]:
|
||||
"""Returns the list of vision input names from the input shapes."""
|
||||
if self._model_data:
|
||||
return list(self._model_data.input_shapes.keys())
|
||||
raise ValueError("Model data is not available. Ensure the model is loaded correctly.")
|
||||
|
||||
@abstractmethod
|
||||
def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict:
|
||||
"""
|
||||
|
||||
@@ -54,6 +54,11 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny
|
||||
self.input_to_dtype[name] = info[2] # dtype
|
||||
self.input_to_device[name] = info[3] # device
|
||||
|
||||
@property
|
||||
def vision_input_names(self) -> list[str]:
|
||||
"""Returns the list of vision input names from the input shapes."""
|
||||
return [name for name in self.input_shapes.keys() if 'img' in name]
|
||||
|
||||
def prepare_vision_inputs(self, imgs_cl: CLMemDict, frames: FrameDict):
|
||||
"""Prepares vision (image) inputs as Tinygrad Tensors."""
|
||||
for key in imgs_cl:
|
||||
@@ -109,6 +114,11 @@ class TinygradSplitRunner(ModelRunner):
|
||||
vision_output = self.vision_runner.run_model()
|
||||
return {**policy_output, **vision_output} # Combine results
|
||||
|
||||
@property
|
||||
def vision_input_names(self) -> list[str]:
|
||||
"""Returns the list of vision input names from the vision runner."""
|
||||
return list(self.vision_runner.vision_input_names)
|
||||
|
||||
@property
|
||||
def input_shapes(self) -> ShapeDict:
|
||||
"""Returns the combined input shapes from both vision and policy models."""
|
||||
|
||||
@@ -1 +1 @@
|
||||
dec34c57c4131f6fca5d1035201d1afbf43e5250cede7bfdc798371af008afad
|
||||
71979b29c4bab3007de1a4265442d79f44c0eaef066af66086dddfc432709b94
|
||||
Submodule sunnypilot/neural_network_data updated: 16946bfd9c...b59ab483c8
47
sunnypilot/selfdrive/car/cruise_ext.py
Normal file
47
sunnypilot/selfdrive/car/cruise_ext.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
from cereal import car
|
||||
from openpilot.common.params import Params
|
||||
|
||||
ButtonType = car.CarState.ButtonEvent.Type
|
||||
|
||||
|
||||
class VCruiseHelperSP:
|
||||
def __init__(self) -> None:
|
||||
self.params = Params()
|
||||
|
||||
self.custom_acc_enabled = self.params.get_bool("CustomAccIncrementsEnabled")
|
||||
self.short_increment = self.read_int_param("CustomAccShortPressIncrement", 1)
|
||||
self.long_increment = self.read_int_param("CustomAccLongPressIncrement", 5)
|
||||
|
||||
def read_int_param(self, key: str, default: int = 0) -> int:
|
||||
try:
|
||||
return int(self.params.get(key, encoding='utf8'))
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
def read_custom_set_speed_params(self) -> None:
|
||||
self.custom_acc_enabled = self.params.get_bool("CustomAccIncrementsEnabled")
|
||||
self.short_increment = self.read_int_param("CustomAccShortPressIncrement", 1)
|
||||
self.long_increment = self.read_int_param("CustomAccLongPressIncrement", 5)
|
||||
|
||||
def update_v_cruise_delta(self, long_press: bool, v_cruise_delta: float) -> tuple[bool, float]:
|
||||
if not self.custom_acc_enabled:
|
||||
v_cruise_delta = v_cruise_delta * (5 if long_press else 1)
|
||||
return long_press, v_cruise_delta
|
||||
|
||||
# Apply user-specified multipliers to the base increment
|
||||
short_increment = np.clip(self.short_increment, 1, 10)
|
||||
long_increment = np.clip(self.long_increment, 1, 10)
|
||||
|
||||
actual_increment = long_increment if long_press else short_increment
|
||||
round_to_nearest = actual_increment in (5, 10)
|
||||
v_cruise_delta = v_cruise_delta * actual_increment
|
||||
|
||||
return round_to_nearest, v_cruise_delta
|
||||
149
sunnypilot/selfdrive/car/tests/test_custom_cruise.py
Normal file
149
sunnypilot/selfdrive/car/tests/test_custom_cruise.py
Normal file
@@ -0,0 +1,149 @@
|
||||
import pytest
|
||||
from parameterized import parameterized_class
|
||||
|
||||
from cereal import car
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.car.cruise import V_CRUISE_INITIAL
|
||||
from openpilot.selfdrive.car.tests.test_cruise_speed import TestVCruiseHelper
|
||||
|
||||
ButtonEvent = car.CarState.ButtonEvent
|
||||
ButtonType = car.CarState.ButtonEvent.Type
|
||||
|
||||
|
||||
@parameterized_class(('pcm_cruise',), [(False,)])
|
||||
class TestCustomAccIncrements(TestVCruiseHelper):
|
||||
def setup_method(self):
|
||||
TestVCruiseHelper.setup_method(self)
|
||||
self.params = Params()
|
||||
self.reset_custom_params()
|
||||
|
||||
def reset_custom_params(self) -> None:
|
||||
"""Reset to default custom ACC parameters"""
|
||||
self.params.put_bool("CustomAccIncrementsEnabled", False)
|
||||
self.params.put("CustomAccShortPressIncrement", "1")
|
||||
self.params.put("CustomAccLongPressIncrement", "5")
|
||||
self.v_cruise_helper.read_custom_set_speed_params()
|
||||
|
||||
def press_button_short(self, button_type: car.CarState.ButtonEvent.Type) -> None:
|
||||
"""Simulate a short button press (press + release)"""
|
||||
CS = car.CarState(cruiseState={"available": True})
|
||||
CS.buttonEvents = [ButtonEvent(type=button_type, pressed=True)]
|
||||
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=True)
|
||||
|
||||
CS.buttonEvents = [ButtonEvent(type=button_type, pressed=False)]
|
||||
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=True)
|
||||
|
||||
def press_button_long(self, button_type: car.CarState.ButtonEvent.Type) -> None:
|
||||
"""Simulate a long button press (50+ frames)"""
|
||||
CS = car.CarState(cruiseState={"available": True})
|
||||
CS.buttonEvents = [ButtonEvent(type=button_type, pressed=True)]
|
||||
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=True)
|
||||
|
||||
# Hold for 50 frames to trigger long press
|
||||
CS.buttonEvents = []
|
||||
for _ in range(50):
|
||||
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=True)
|
||||
|
||||
CS.buttonEvents = [ButtonEvent(type=button_type, pressed=False)]
|
||||
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=True)
|
||||
|
||||
def set_custom_increments(self, enabled: bool, short_inc: int, long_inc: int) -> None:
|
||||
"""Set custom ACC increment parameters"""
|
||||
self.params.put_bool("CustomAccIncrementsEnabled", enabled)
|
||||
self.params.put("CustomAccShortPressIncrement", str(short_inc))
|
||||
self.params.put("CustomAccLongPressIncrement", str(long_inc))
|
||||
self.v_cruise_helper.read_custom_set_speed_params()
|
||||
|
||||
def test_default_behavior_when_disabled(self):
|
||||
"""Test that default increments are used when custom ACC is disabled"""
|
||||
self.set_custom_increments(enabled=False, short_inc=5, long_inc=10)
|
||||
self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False, False)
|
||||
|
||||
initial_speed = self.v_cruise_helper.v_cruise_kph
|
||||
|
||||
# Short press should increment by 1 (default)
|
||||
self.press_button_short(ButtonType.accelCruise)
|
||||
assert self.v_cruise_helper.v_cruise_kph == initial_speed + 1
|
||||
|
||||
@pytest.mark.parametrize("increment", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
|
||||
def test_custom_short_press_increments(self, increment):
|
||||
"""Test custom short press increments (1-10)"""
|
||||
self.set_custom_increments(enabled=True, short_inc=increment, long_inc=5)
|
||||
self.enable(50 * CV.KPH_TO_MS, False, False)
|
||||
|
||||
initial_speed = self.v_cruise_helper.v_cruise_kph
|
||||
self.press_button_short(ButtonType.accelCruise)
|
||||
|
||||
if increment in (5, 10):
|
||||
# Should round to nearest increment
|
||||
expected_speed = ((initial_speed // increment) + 1) * increment
|
||||
else:
|
||||
expected_speed = initial_speed + increment
|
||||
|
||||
assert self.v_cruise_helper.v_cruise_kph == expected_speed
|
||||
|
||||
@pytest.mark.parametrize("increment", (1, 5, 10))
|
||||
def test_custom_long_press_increments(self, increment):
|
||||
"""Test custom long press increments (1, 5, 10)"""
|
||||
self.set_custom_increments(enabled=True, short_inc=1, long_inc=increment)
|
||||
self.enable(50 * CV.KPH_TO_MS, False, False)
|
||||
|
||||
initial_speed = self.v_cruise_helper.v_cruise_kph
|
||||
self.press_button_long(ButtonType.accelCruise)
|
||||
|
||||
if increment in (5, 10):
|
||||
# Should round to nearest increment
|
||||
expected_speed = ((initial_speed // increment) + 1) * increment
|
||||
else:
|
||||
expected_speed = initial_speed + increment
|
||||
|
||||
assert self.v_cruise_helper.v_cruise_kph == expected_speed
|
||||
|
||||
@pytest.mark.parametrize("button_type", [ButtonType.accelCruise, ButtonType.decelCruise])
|
||||
def test_accel_decel_symmetry(self, button_type):
|
||||
"""Test that acceleration and deceleration work symmetrically"""
|
||||
self.set_custom_increments(enabled=True, short_inc=3, long_inc=5)
|
||||
self.enable(50 * CV.KPH_TO_MS, False, False)
|
||||
|
||||
initial_speed = self.v_cruise_helper.v_cruise_kph
|
||||
self.press_button_short(button_type)
|
||||
|
||||
expected_change = 3 if button_type == ButtonType.accelCruise else -3
|
||||
assert self.v_cruise_helper.v_cruise_kph == initial_speed + expected_change
|
||||
|
||||
def test_rounding_behavior(self):
|
||||
"""Test rounding behavior for 5 and 10 increments"""
|
||||
test_cases = [
|
||||
(47, 5, 50), # 47 -> 50 (round up to next 5)
|
||||
(45, 5, 50), # 45 -> 50 (already at 5, increment by 5)
|
||||
(43, 10, 50), # 43 -> 50 (round up to next 10)
|
||||
(40, 10, 50), # 40 -> 50 (already at 10, increment by 10)
|
||||
]
|
||||
|
||||
for initial, increment, expected in test_cases:
|
||||
self.set_custom_increments(enabled=True, short_inc=increment, long_inc=increment)
|
||||
self.reset_cruise_speed_state()
|
||||
self.enable(initial * CV.KPH_TO_MS, False, False)
|
||||
|
||||
self.press_button_short(ButtonType.accelCruise)
|
||||
assert self.v_cruise_helper.v_cruise_kph == expected
|
||||
|
||||
def test_invalid_values_fallback(self):
|
||||
"""Test that invalid values fallback to safe defaults"""
|
||||
# Test invalid short increment
|
||||
self.set_custom_increments(enabled=True, short_inc=-1, long_inc=5)
|
||||
self.enable(50 * CV.KPH_TO_MS, False, False)
|
||||
|
||||
initial_speed = self.v_cruise_helper.v_cruise_kph
|
||||
self.press_button_short(ButtonType.accelCruise)
|
||||
assert self.v_cruise_helper.v_cruise_kph == initial_speed + 1 # Should fallback to 1
|
||||
|
||||
# Test invalid long increment
|
||||
self.reset_cruise_speed_state()
|
||||
self.set_custom_increments(enabled=True, short_inc=1, long_inc=99)
|
||||
self.enable(50 * CV.KPH_TO_MS, False, False)
|
||||
|
||||
initial_speed = self.v_cruise_helper.v_cruise_kph
|
||||
self.press_button_long(ButtonType.accelCruise)
|
||||
assert self.v_cruise_helper.v_cruise_kph == initial_speed + 10 # Should fallback to 10
|
||||
@@ -11,7 +11,7 @@ from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
|
||||
LAT_PLAN_MIN_IDX = 5
|
||||
|
||||
LATERAL_LAG_MOD = 0.0 # seconds, modifies how far in the future we look ahead for the lateral plan
|
||||
|
||||
def get_predicted_lateral_jerk(lat_accels, t_diffs):
|
||||
# compute finite difference between subsequent model_v2.acceleration.y values
|
||||
@@ -85,12 +85,15 @@ class LatControlTorqueExtBase:
|
||||
|
||||
# precompute time differences between ModelConstants.T_IDXS
|
||||
self.t_diffs = np.diff(ModelConstants.T_IDXS)
|
||||
self.desired_lat_jerk_time = CP.steerActuatorDelay + 0.3
|
||||
self.desired_lat_jerk_time = CP.steerActuatorDelay + LATERAL_LAG_MOD
|
||||
|
||||
def update_model_v2(self, model_v2):
|
||||
self.model_v2 = model_v2
|
||||
self.model_valid = self.model_v2 is not None and len(self.model_v2.orientation.x) >= CONTROL_N
|
||||
|
||||
def update_lateral_lag(self, lag):
|
||||
self.desired_lat_jerk_time = max(0.01, lag) + LATERAL_LAG_MOD
|
||||
|
||||
def update_friction_input(self, val_1, val_2):
|
||||
_error = val_1 - val_2
|
||||
_value = self.lat_accel_friction_factor * _error + self.lat_jerk_friction_factor * self.lookahead_lateral_jerk
|
||||
|
||||
@@ -45,9 +45,8 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
|
||||
self.pitch_last = 0.0
|
||||
|
||||
# setup future time offsets
|
||||
self.nn_time_offset = CP.steerActuatorDelay + 0.2
|
||||
future_times = [0.3, 0.6, 1.0, 1.5] # seconds in the future
|
||||
self.nn_future_times = [i + self.nn_time_offset for i in future_times]
|
||||
self.future_times = [0.3, 0.6, 1.0, 1.5] # seconds in the future
|
||||
self.nn_future_times = [i + self.desired_lat_jerk_time for i in self.future_times]
|
||||
|
||||
# setup past time offsets
|
||||
self.past_times = [-0.3, -0.2, -0.1]
|
||||
@@ -58,6 +57,10 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
|
||||
self.error_deque = deque(maxlen=history_check_frames[0])
|
||||
self.past_future_len = len(self.past_times) + len(self.nn_future_times)
|
||||
|
||||
def update_lateral_lag(self, lag):
|
||||
super().update_lateral_lag(lag)
|
||||
self.nn_future_times = [t + self.desired_lat_jerk_time for t in self.future_times]
|
||||
|
||||
def update_neural_network_feedforward(self, CS, params, calibrated_pose) -> None:
|
||||
if not self.enabled or not self.model_valid or not self.has_nn_model:
|
||||
return
|
||||
|
||||
@@ -73,17 +73,21 @@ class TestNeuralNetworkLateralControl:
|
||||
controller.extension.model_v2 = model_v2
|
||||
|
||||
# Saturate for curvature limited and controller limited
|
||||
test_lag = 0.3
|
||||
for _ in range(1000):
|
||||
controller.extension.update_model_v2(model_v2)
|
||||
controller.extension.update_lateral_lag(test_lag)
|
||||
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True)
|
||||
assert lac_log.saturated
|
||||
|
||||
for _ in range(1000):
|
||||
controller.extension.update_model_v2(model_v2)
|
||||
controller.extension.update_lateral_lag(test_lag)
|
||||
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False)
|
||||
assert not lac_log.saturated
|
||||
|
||||
for _ in range(1000):
|
||||
controller.extension.update_model_v2(model_v2)
|
||||
controller.extension.update_lateral_lag(test_lag)
|
||||
_, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False)
|
||||
assert lac_log.saturated
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user