mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-09-15 17:23:42 +08:00
Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e99599d51 | |||
| 92f1977666 | |||
| af53db3b07 | |||
| 394603727e | |||
| d0bd8cc4a3 | |||
| 00eb0e983d | |||
| 9bdfd46b8f | |||
| cdf990c1b1 | |||
| ea8eaed1aa | |||
| 4e094bc740 | |||
| 17432e1b0d | |||
| 0218ae82ed | |||
| 7b35f64049 | |||
| 0a254fbc4e | |||
| 903f426bb9 | |||
| 53d757a84f | |||
| fa5fce465a | |||
| d1893ee3eb | |||
| 56fca1353f | |||
| a22eecd773 | |||
| 01b3f70c01 | |||
| 8b8f33f488 | |||
| d5b5383f1a | |||
| 91792aa767 | |||
| c1e0b87059 | |||
| 7f6f346c38 | |||
| 5e3fc13751 | |||
| 885f3f73e0 | |||
| 2c78cfe200 | |||
| 4a4f3fce94 | |||
| 5772683432 | |||
| 6a37d8a89e | |||
| 87a6e369aa | |||
| 5f3d876aaa | |||
| 5f559cfcc7 | |||
| 42fc89a0e5 | |||
| ccd55d3663 | |||
| 25f5ec46d9 | |||
| c460f5150f | |||
| b18037c38a | |||
| 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 | |||
| 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/
|
||||||
.venv/
|
.venv/
|
||||||
|
**/.idea
|
||||||
|
**/.hypothesis
|
||||||
|
**/.mypy_cache
|
||||||
|
|
||||||
|
**/.venv
|
||||||
|
**/.venv/
|
||||||
|
|
||||||
|
**/.ci_cache
|
||||||
|
**/*.rlog
|
||||||
|
|
||||||
|
**/Dockerfile*
|
||||||
|
**/dockerfile*
|
||||||
|
**/build_output
|
||||||
|
|
||||||
notebooks
|
notebooks
|
||||||
phone
|
phone
|
||||||
|
|||||||
@@ -54,8 +54,9 @@ jobs:
|
|||||||
git add .
|
git add .
|
||||||
- name: update car docs
|
- name: update car docs
|
||||||
run: |
|
run: |
|
||||||
|
export PYTHONPATH="$PWD"
|
||||||
scons -j$(nproc) --minimal opendbc_repo
|
scons -j$(nproc) --minimal opendbc_repo
|
||||||
PYTHONPATH=. python selfdrive/car/docs.py
|
python selfdrive/car/docs.py
|
||||||
git add docs/CARS.md
|
git add docs/CARS.md
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
|
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ FROM ghcr.io/commaai/openpilot-base:latest
|
|||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
ENV OPENPILOT_PATH=/home/batman/openpilot
|
ENV OPENPILOT_PATH=/home/batman/openpilot
|
||||||
ENV PYTHONPATH=${OPENPILOT_PATH}:${PYTHONPATH}
|
|
||||||
|
|
||||||
RUN mkdir -p ${OPENPILOT_PATH}
|
RUN mkdir -p ${OPENPILOT_PATH}
|
||||||
WORKDIR ${OPENPILOT_PATH}
|
WORKDIR ${OPENPILOT_PATH}
|
||||||
|
|||||||
@@ -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));
|
points @10 :List(List(Float32));
|
||||||
version @11 :Int32;
|
version @11 :Int32;
|
||||||
useParams @12 :Bool;
|
useParams @12 :Bool;
|
||||||
|
calPerc @13 :Int8;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LiveDelayData {
|
struct LiveDelayData {
|
||||||
@@ -2291,6 +2292,7 @@ struct LiveDelayData {
|
|||||||
lateralDelayEstimate @3 :Float32;
|
lateralDelayEstimate @3 :Float32;
|
||||||
lateralDelayEstimateStd @5 :Float32;
|
lateralDelayEstimateStd @5 :Float32;
|
||||||
points @4 :List(Float32);
|
points @4 :List(Float32);
|
||||||
|
calPerc @6 :Int8;
|
||||||
|
|
||||||
enum Status {
|
enum Status {
|
||||||
unestimated @0;
|
unestimated @0;
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
#define DEFAULT_MODEL "Filet o Fish (Default)"
|
#define DEFAULT_MODEL "Vegetarian Filet o Fish (Default)"
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
|
|||||||
{"CustomAccShortPressIncrement", PERSISTENT | BACKUP},
|
{"CustomAccShortPressIncrement", PERSISTENT | BACKUP},
|
||||||
{"DeviceBootMode", PERSISTENT | BACKUP},
|
{"DeviceBootMode", PERSISTENT | BACKUP},
|
||||||
{"EnableGithubRunner", PERSISTENT | BACKUP},
|
{"EnableGithubRunner", PERSISTENT | BACKUP},
|
||||||
|
{"InteractivityTimeout", PERSISTENT | BACKUP},
|
||||||
{"MaxTimeOffroad", PERSISTENT | BACKUP},
|
{"MaxTimeOffroad", PERSISTENT | BACKUP},
|
||||||
{"Brightness", PERSISTENT | BACKUP},
|
{"Brightness", PERSISTENT | BACKUP},
|
||||||
{"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION},
|
{"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION},
|
||||||
@@ -178,6 +179,8 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
|
|||||||
|
|
||||||
// model panel params
|
// model panel params
|
||||||
{"LagdToggle", PERSISTENT | BACKUP},
|
{"LagdToggle", PERSISTENT | BACKUP},
|
||||||
|
{"LagdToggleDesc", PERSISTENT},
|
||||||
|
{"LagdToggledelay", PERSISTENT | BACKUP},
|
||||||
|
|
||||||
// mapd
|
// mapd
|
||||||
{"MapAdvisorySpeedLimit", CLEAR_ON_ONROAD_TRANSITION},
|
{"MapAdvisorySpeedLimit", CLEAR_ON_ONROAD_TRANSITION},
|
||||||
|
|||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from openpilot.common.basedir import BASEDIR
|
||||||
|
|
||||||
|
|
||||||
|
class Spinner:
|
||||||
|
def __init__(self):
|
||||||
|
try:
|
||||||
|
self.spinner_proc = subprocess.Popen(["./spinner.py"],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
cwd=os.path.join(BASEDIR, "system", "ui"),
|
||||||
|
close_fds=True)
|
||||||
|
except OSError:
|
||||||
|
self.spinner_proc = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def update(self, spinner_text: str):
|
||||||
|
if self.spinner_proc is not None:
|
||||||
|
self.spinner_proc.stdin.write(spinner_text.encode('utf8') + b"\n")
|
||||||
|
try:
|
||||||
|
self.spinner_proc.stdin.flush()
|
||||||
|
except BrokenPipeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update_progress(self, cur: float, total: float):
|
||||||
|
self.update(str(round(100 * cur / total)))
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.spinner_proc is not None:
|
||||||
|
self.spinner_proc.kill()
|
||||||
|
try:
|
||||||
|
self.spinner_proc.communicate(timeout=2.)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
print("WARNING: failed to kill spinner")
|
||||||
|
self.spinner_proc = None
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import time
|
||||||
|
with Spinner() as s:
|
||||||
|
s.update("Spinner text")
|
||||||
|
time.sleep(5.0)
|
||||||
|
print("gone")
|
||||||
|
time.sleep(5.0)
|
||||||
Executable
+63
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import subprocess
|
||||||
|
from openpilot.common.basedir import BASEDIR
|
||||||
|
|
||||||
|
|
||||||
|
class TextWindow:
|
||||||
|
def __init__(self, text):
|
||||||
|
try:
|
||||||
|
self.text_proc = subprocess.Popen(["./text.py", text],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
cwd=os.path.join(BASEDIR, "system", "ui"),
|
||||||
|
close_fds=True)
|
||||||
|
except OSError:
|
||||||
|
self.text_proc = None
|
||||||
|
|
||||||
|
def get_status(self):
|
||||||
|
if self.text_proc is not None:
|
||||||
|
self.text_proc.poll()
|
||||||
|
return self.text_proc.returncode
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.text_proc is not None:
|
||||||
|
self.text_proc.terminate()
|
||||||
|
self.text_proc = None
|
||||||
|
|
||||||
|
def wait_for_exit(self):
|
||||||
|
if self.text_proc is not None:
|
||||||
|
while True:
|
||||||
|
if self.get_status() == 1:
|
||||||
|
return
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
text = """Traceback (most recent call last):
|
||||||
|
File "./controlsd.py", line 608, in <module>
|
||||||
|
main()
|
||||||
|
File "./controlsd.py", line 604, in main
|
||||||
|
controlsd_thread(sm, pm, logcan)
|
||||||
|
File "./controlsd.py", line 455, in controlsd_thread
|
||||||
|
1/0
|
||||||
|
ZeroDivisionError: division by zero"""
|
||||||
|
print(text)
|
||||||
|
|
||||||
|
with TextWindow(text) as s:
|
||||||
|
for _ in range(100):
|
||||||
|
if s.get_status() == 1:
|
||||||
|
print("Got exit button")
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
print("gone")
|
||||||
+8
-8
@@ -15,7 +15,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
|||||||
|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=A3 2014-19">Buy Here</a></sub></details>|||
|
|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=A3 2014-19">Buy Here</a></sub></details>|||
|
||||||
|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=A3 Sportback e-tron 2017-18">Buy Here</a></sub></details>|||
|
|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=A3 Sportback e-tron 2017-18">Buy Here</a></sub></details>|||
|
||||||
|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=Q2 2018">Buy Here</a></sub></details>|||
|
|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=Q2 2018">Buy Here</a></sub></details>|||
|
||||||
|Audi|Q3 2019-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|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=RS3 2018">Buy Here</a></sub></details>|||
|
||||||
|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=S3 2015-17">Buy Here</a></sub></details>|||
|
|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Audi&model=S3 2015-17">Buy Here</a></sub></details>|||
|
||||||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
@@ -38,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 Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape Hybrid 2023-24">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|
||||||
|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape Plug-in Hybrid 2020-22">Buy Here</a></sub></details>|||
|
|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape Plug-in Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||||
|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape Plug-in Hybrid 2023-24">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|
|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape Plug-in Hybrid 2023-24">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|
||||||
|
|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Expedition 2022-24">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=MewJc9LYp9M|
|
||||||
|Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Explorer 2020-24">Buy Here</a></sub></details>|||
|
|Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Explorer 2020-24">Buy Here</a></sub></details>|||
|
||||||
|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Explorer Hybrid 2020-24">Buy Here</a></sub></details>|||
|
|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Explorer Hybrid 2020-24">Buy Here</a></sub></details>|||
|
||||||
|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=F-150 2021-23">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=MewJc9LYp9M|
|
|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=F-150 2021-23">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=MewJc9LYp9M|
|
||||||
@@ -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 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick 2023-24">Buy Here</a></sub></details>|||
|
||||||
|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick Hybrid 2022">Buy Here</a></sub></details>|||
|
|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick Hybrid 2022">Buy Here</a></sub></details>|||
|
||||||
|Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick Hybrid 2023-24">Buy Here</a></sub></details>|||
|
|Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick Hybrid 2023-24">Buy Here</a></sub></details>|||
|
||||||
|Ford|Mustang Mach-E 2021-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|
|
|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Ranger 2024">Buy Here</a></sub></details>||https://www.youtube.com/watch?v=uUGkH6C_EQU|
|
||||||
|Genesis|G70 2018|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G70 2018">Buy Here</a></sub></details>|||
|
|Genesis|G70 2018|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G70 2018">Buy Here</a></sub></details>|||
|
||||||
|Genesis|G70 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G70 2019-21">Buy Here</a></sub></details>|||
|
|Genesis|G70 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G70 2019-21">Buy Here</a></sub></details>|||
|
||||||
@@ -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 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Honda|Civic Hatchback 2017-21|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback 2017-21">Buy Here</a></sub></details>|||
|
|Honda|Civic Hatchback 2017-21|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback 2017-21">Buy Here</a></sub></details>|||
|
||||||
|Honda|Civic Hatchback 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Honda|Civic Hatchback 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Honda|Civic Hatchback Hybrid 2023 (Europe only)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback Hybrid 2023 (Europe only)">Buy Here</a></sub></details>|||
|
|
||||||
|Honda|Civic Hatchback Hybrid 2025|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback Hybrid 2025">Buy Here</a></sub></details>|||
|
|Honda|Civic Hatchback Hybrid 2025|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback Hybrid 2025">Buy Here</a></sub></details>|||
|
||||||
|
|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback Hybrid (Europe only) 2023">Buy Here</a></sub></details>|||
|
||||||
|Honda|CR-V 2015-16|Touring Trim|openpilot|26 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V 2015-16">Buy Here</a></sub></details>|||
|
|Honda|CR-V 2015-16|Touring Trim|openpilot|26 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V 2015-16">Buy Here</a></sub></details>|||
|
||||||
|Honda|CR-V 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V 2017-22">Buy Here</a></sub></details>|||
|
|Honda|CR-V 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V 2017-22">Buy Here</a></sub></details>|||
|
||||||
|Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V Hybrid 2017-22">Buy Here</a></sub></details>|||
|
|Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V Hybrid 2017-22">Buy Here</a></sub></details>|||
|
||||||
@@ -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 2019|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|32 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Ioniq Plug-in Hybrid 2019">Buy Here</a></sub></details>|||
|
||||||
|Hyundai|Ioniq Plug-in Hybrid 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Ioniq Plug-in Hybrid 2020-22">Buy Here</a></sub></details>|||
|
|Hyundai|Ioniq Plug-in Hybrid 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Ioniq Plug-in Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||||
|Hyundai|Kona 2020|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|6 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona 2020">Buy Here</a></sub></details>|||
|
|Hyundai|Kona 2020|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|6 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona 2020">Buy Here</a></sub></details>|||
|
||||||
|Hyundai|Kona 2022|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai O connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona 2022">Buy Here</a></sub></details>|||
|
|
||||||
|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona Electric 2018-21">Buy Here</a></sub></details>|||
|
|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona Electric 2018-21">Buy Here</a></sub></details>|||
|
||||||
|Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai O connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona Electric 2022-23">Buy Here</a></sub></details>|||
|
|Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai O connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona Electric 2022-23">Buy Here</a></sub></details>|||
|
||||||
|Hyundai|Kona Electric (with HDA II, Korea only) 2023[<sup>6</sup>](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai R connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona Electric (with HDA II, Korea only) 2023">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=U2fOCmcQ8hw" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Hyundai|Kona Electric (with HDA II, Korea only) 2023[<sup>6</sup>](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai R connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Kona Electric (with HDA II, Korea only) 2023">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=U2fOCmcQ8hw" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
@@ -181,12 +181,12 @@ A supported vehicle is one that just works when you install a comma device. All
|
|||||||
|Kia|Telluride 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Kia&model=Telluride 2020-22">Buy Here</a></sub></details>|||
|
|Kia|Telluride 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Kia&model=Telluride 2020-22">Buy Here</a></sub></details>|||
|
||||||
|Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=CT Hybrid 2017-18">Buy Here</a></sub></details>|||
|
|Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=CT Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||||
|Lexus|ES 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=ES 2017-18">Buy Here</a></sub></details>|||
|
|Lexus|ES 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=ES 2017-18">Buy Here</a></sub></details>|||
|
||||||
|Lexus|ES 2019-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 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=ES Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||||
|Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=ES Hybrid 2019-25">Buy Here</a></sub></details>|<a href="https://youtu.be/BZ29osRVJeg?t=12" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=ES Hybrid 2019-25">Buy Here</a></sub></details>|<a href="https://youtu.be/BZ29osRVJeg?t=12" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Lexus|GS F 2016|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=GS F 2016">Buy Here</a></sub></details>|||
|
|Lexus|GS F 2016|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=GS F 2016">Buy Here</a></sub></details>|||
|
||||||
|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=IS 2017-19">Buy Here</a></sub></details>|||
|
|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=IS 2017-19">Buy Here</a></sub></details>|||
|
||||||
|Lexus|IS 2022-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|LC 2024|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=LC 2024">Buy Here</a></sub></details>|||
|
||||||
|Lexus|NX 2018-19|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX 2018-19">Buy Here</a></sub></details>|||
|
|Lexus|NX 2018-19|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX 2018-19">Buy Here</a></sub></details>|||
|
||||||
|Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX 2020-21">Buy Here</a></sub></details>|||
|
|Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX 2020-21">Buy Here</a></sub></details>|||
|
||||||
@@ -313,11 +313,11 @@ A supported vehicle is one that just works when you install a comma device. All
|
|||||||
|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Polo GTI 2018-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Polo GTI 2018-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
||||||
|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=T-Cross 2021">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=T-Cross 2021">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
||||||
|Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=T-Roc 2018-23">Buy Here</a></sub></details>|||
|
|Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=T-Roc 2018-23">Buy Here</a></sub></details>|||
|
||||||
|Volkswagen|Taos 2022-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 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Teramont 2018-22">Buy Here</a></sub></details>|||
|
||||||
|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Teramont Cross Sport 2021-22">Buy Here</a></sub></details>|||
|
|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Teramont Cross Sport 2021-22">Buy Here</a></sub></details>|||
|
||||||
|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Teramont X 2021-22">Buy Here</a></sub></details>|||
|
|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Teramont X 2021-22">Buy Here</a></sub></details>|||
|
||||||
|Volkswagen|Tiguan 2018-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|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Tiguan eHybrid 2021-23">Buy Here</a></sub></details>|||
|
||||||
|Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Touran 2016-23">Buy Here</a></sub></details>|||
|
|Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Touran 2016-23">Buy Here</a></sub></details>|||
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ Development is coordinated through [Discord](https://discord.comma.ai) and GitHu
|
|||||||
### Getting Started
|
### Getting Started
|
||||||
|
|
||||||
* Setup your [development environment](../tools/)
|
* Setup your [development environment](../tools/)
|
||||||
* Read about the [development workflow](WORKFLOW.md)
|
|
||||||
* Join our [Discord](https://discord.comma.ai)
|
* Join our [Discord](https://discord.comma.ai)
|
||||||
* Docs are at https://docs.comma.ai and https://blog.comma.ai
|
* Docs are at https://docs.comma.ai and https://blog.comma.ai
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
# openpilot development workflow
|
|
||||||
|
|
||||||
Aside from the ML models, most tools used for openpilot development are in this repo.
|
|
||||||
|
|
||||||
Most development happens on normal Ubuntu workstations, and not in cars or directly on comma devices. See the [setup guide](../tools) for getting your PC setup for openpilot development.
|
|
||||||
|
|
||||||
## Quick start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# get the latest stuff
|
|
||||||
git pull
|
|
||||||
git lfs pull
|
|
||||||
git submodule update --init --recursive
|
|
||||||
|
|
||||||
# update dependencies
|
|
||||||
tools/ubuntu_setup.sh
|
|
||||||
|
|
||||||
# build everything
|
|
||||||
scons -j$(nproc)
|
|
||||||
|
|
||||||
# build just the ui with either of these
|
|
||||||
scons -j8 selfdrive/ui/
|
|
||||||
cd selfdrive/ui/ && scons -u -j8
|
|
||||||
|
|
||||||
# test everything
|
|
||||||
pytest
|
|
||||||
|
|
||||||
# test just logging services
|
|
||||||
cd system/loggerd && pytest .
|
|
||||||
|
|
||||||
# run the linter
|
|
||||||
op lint
|
|
||||||
```
|
|
||||||
+1
-1
@@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1
|
|||||||
export VECLIB_MAXIMUM_THREADS=1
|
export VECLIB_MAXIMUM_THREADS=1
|
||||||
|
|
||||||
if [ -z "$AGNOS_VERSION" ]; then
|
if [ -z "$AGNOS_VERSION" ]; then
|
||||||
export AGNOS_VERSION="12.3"
|
export AGNOS_VERSION="12.4"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export STAGING_ROOT="/data/safe_staging"
|
export STAGING_ROOT="/data/safe_staging"
|
||||||
|
|||||||
+1
-1
Submodule opendbc_repo updated: 3bd2bba8a7...c87940a6b5
+1
-1
Submodule panda updated: 5ac4fa5bb0...c33cfa0803
+6
-2
@@ -68,6 +68,9 @@ dependencies = [
|
|||||||
|
|
||||||
# logreader
|
# logreader
|
||||||
"zstandard",
|
"zstandard",
|
||||||
|
|
||||||
|
# ui
|
||||||
|
"qrcode",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
@@ -85,7 +88,8 @@ testing = [
|
|||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-cpp",
|
"pytest-cpp",
|
||||||
"pytest-subtests",
|
"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-timeout",
|
||||||
"pytest-randomly",
|
"pytest-randomly",
|
||||||
"pytest-asyncio",
|
"pytest-asyncio",
|
||||||
@@ -102,8 +106,8 @@ dev = [
|
|||||||
"azure-storage-blob",
|
"azure-storage-blob",
|
||||||
"dbus-next",
|
"dbus-next",
|
||||||
"dictdiffer",
|
"dictdiffer",
|
||||||
"lru-dict",
|
|
||||||
"matplotlib",
|
"matplotlib",
|
||||||
|
"opencv-python-headless",
|
||||||
"parameterized >=0.8, <0.9",
|
"parameterized >=0.8, <0.9",
|
||||||
"pyautogui",
|
"pyautogui",
|
||||||
"pygame",
|
"pygame",
|
||||||
|
|||||||
+1
-1
Submodule rednose_repo updated: 8b86052919...7fddc8e6d4
+15
-20
@@ -1,36 +1,31 @@
|
|||||||
# openpilot releases
|
# openpilot releases
|
||||||
|
|
||||||
|
```
|
||||||
## release checklist
|
## release checklist
|
||||||
|
|
||||||
**Go to `devel-staging`**
|
**Go to `devel-staging`**
|
||||||
|
- [ ] update RELEASES.md
|
||||||
- [ ] update `devel-staging`: `git reset --hard origin/master-ci`
|
- [ ] update `devel-staging`: `git reset --hard origin/master-ci`
|
||||||
- [ ] open a pull request from `devel-staging` to `devel`
|
- [ ] open a pull request from `devel-staging` to `devel`
|
||||||
|
- [ ] post on Discord
|
||||||
|
|
||||||
**Go to `devel`**
|
**Go to `devel`**
|
||||||
- [ ] update RELEASES.md
|
|
||||||
- [ ] close out milestone
|
|
||||||
- [ ] post on Discord dev channel
|
|
||||||
- [ ] bump version on master: `common/version.h` and `RELEASES.md`
|
- [ ] bump version on master: `common/version.h` and `RELEASES.md`
|
||||||
- [ ] merge the pull request
|
- [ ] before merging the pull request
|
||||||
|
- [ ] update from previous release -> new release
|
||||||
tests:
|
- [ ] update from new release -> previous release
|
||||||
- [ ] update from previous release -> new release
|
- [ ] fresh install with `openpilot-test.comma.ai`
|
||||||
- [ ] update from new release -> previous release
|
- [ ] drive on fresh install
|
||||||
- [ ] fresh install with `openpilot-test.comma.ai`
|
- [ ] no submodules or LFS
|
||||||
- [ ] drive on fresh install
|
- [ ] check sentry, MTBF, etc.
|
||||||
- [ ] comma body test
|
|
||||||
- [ ] no submodules or LFS
|
|
||||||
- [ ] check sentry, MTBF, etc.
|
|
||||||
|
|
||||||
**Go to `release3`**
|
**Go to `release3`**
|
||||||
- [ ] publish the blog post
|
- [ ] publish the blog post
|
||||||
- [ ] `git reset --hard origin/release3-staging`
|
- [ ] `git reset --hard origin/release3-staging`
|
||||||
- [ ] tag the release
|
- [ ] tag the release: `git tag v0.X.X <commit-hash> && git push origin v0.X.X`
|
||||||
```
|
|
||||||
git tag v0.X.X <commit-hash>
|
|
||||||
git push origin v0.X.X
|
|
||||||
```
|
|
||||||
- [ ] create GitHub release
|
- [ ] create GitHub release
|
||||||
- [ ] final test install on `openpilot.comma.ai`
|
- [ ] final test install on `openpilot.comma.ai`
|
||||||
- [ ] update production
|
- [ ] update factory provisioning
|
||||||
- [ ] Post on Discord, X, etc.
|
- [ ] close out milestone
|
||||||
|
- [ ] post on Discord, X, etc.
|
||||||
|
```
|
||||||
|
|||||||
@@ -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}"
|
||||||
@@ -5,6 +5,7 @@ set -e
|
|||||||
DEFAULT_REPO_URL="https://github.com/sunnypilot"
|
DEFAULT_REPO_URL="https://github.com/sunnypilot"
|
||||||
START_AT_BOOT=false
|
START_AT_BOOT=false
|
||||||
RESTORE_MODE=false
|
RESTORE_MODE=false
|
||||||
|
RUNNER_VERSION="2.325.0"
|
||||||
|
|
||||||
# Parse command line arguments
|
# Parse command line arguments
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
@@ -108,9 +109,9 @@ setup_system_configs() {
|
|||||||
install_runner() {
|
install_runner() {
|
||||||
echo "Downloading and setting up runner..."
|
echo "Downloading and setting up runner..."
|
||||||
cd "$RUNNER_DIR"
|
cd "$RUNNER_DIR"
|
||||||
curl -o actions-runner-linux-arm64-2.322.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.322.0/actions-runner-linux-arm64-2.322.0.tar.gz
|
curl -o actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz -L https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz
|
||||||
sudo -u ${RUNNER_USER} tar -xzf ./actions-runner-linux-arm64-2.322.0.tar.gz
|
sudo -u ${RUNNER_USER} tar -xzf ./actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz
|
||||||
sudo rm ./actions-runner-linux-arm64-2.322.0.tar.gz
|
sudo rm ./actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz
|
||||||
sudo chmod +x ./config.sh
|
sudo chmod +x ./config.sh
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,25 +150,32 @@ EOL
|
|||||||
}
|
}
|
||||||
|
|
||||||
install_service() {
|
install_service() {
|
||||||
|
local service_name
|
||||||
|
if [ -f "${RUNNER_DIR}/.service" ]; then
|
||||||
|
service_name=$(cat "${RUNNER_DIR}/.service")
|
||||||
|
else
|
||||||
|
service_name="actions.runner.sunnypilot.$(uname -n)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
create_service_template
|
||||||
remount_rw
|
remount_rw
|
||||||
|
local service_path="/etc/systemd/system/${service_name}"
|
||||||
echo "Installing systemd service..."
|
echo "Installing systemd service..."
|
||||||
|
if [ -f "${service_path}" ]; then
|
||||||
|
echo "Service ${service_path} found in systemd, we will delete it"
|
||||||
|
sudo rm -f "${service_path}"
|
||||||
|
fi
|
||||||
|
|
||||||
cd "$RUNNER_DIR"
|
cd "$RUNNER_DIR"
|
||||||
sudo ./svc.sh install $RUNNER_USER
|
sudo ./svc.sh install $RUNNER_USER
|
||||||
|
|
||||||
if [ "$START_AT_BOOT" = false ]; then
|
if [ "$START_AT_BOOT" = false ]; then
|
||||||
local service_name
|
|
||||||
if [ -f "${RUNNER_DIR}/.service" ]; then
|
|
||||||
service_name=$(cat "${RUNNER_DIR}/.service")
|
|
||||||
else
|
|
||||||
service_name="actions.runner.sunnypilot.$(uname -n)"
|
|
||||||
fi
|
|
||||||
sudo systemctl disable "${service_name}"
|
sudo systemctl disable "${service_name}"
|
||||||
fi
|
fi
|
||||||
remount_ro
|
remount_ro
|
||||||
}
|
}
|
||||||
|
|
||||||
check_restore_prerequisites() {
|
check_restore_prerequisites() {
|
||||||
local needs_restore=false
|
|
||||||
local can_restore=false
|
local can_restore=false
|
||||||
local service_name=""
|
local service_name=""
|
||||||
|
|
||||||
@@ -189,25 +197,16 @@ check_restore_prerequisites() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Then check if restoration is needed (if either service or user is missing)
|
|
||||||
if ! systemctl list-unit-files "${service_name}" &>/dev/null; then
|
|
||||||
echo "Service ${service_name} not found in systemd"
|
|
||||||
needs_restore=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! id "${RUNNER_USER}" &>/dev/null; then
|
if ! id "${RUNNER_USER}" &>/dev/null; then
|
||||||
echo "User ${RUNNER_USER} does not exist"
|
echo "User ${RUNNER_USER} does not exist"
|
||||||
needs_restore=true
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Only proceed if we can restore AND need to restore
|
# Only proceed if we can restore AND need to restore
|
||||||
if [ "$can_restore" = true ] && [ "$needs_restore" = true ]; then
|
if [ "$can_restore" = true ]; then
|
||||||
echo "Restoration is needed and possible"
|
echo "Restoration is possible"
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
if [ "$needs_restore" = false ]; then
|
echo "No restoration possible"
|
||||||
echo "System is already properly configured (user and service exist)"
|
|
||||||
fi
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -226,7 +225,6 @@ perform_install() {
|
|||||||
setup_system_configs
|
setup_system_configs
|
||||||
install_runner
|
install_runner
|
||||||
set_directory_permissions
|
set_directory_permissions
|
||||||
create_service_template
|
|
||||||
configure_runner
|
configure_runner
|
||||||
install_service
|
install_service
|
||||||
echo "Installation completed successfully"
|
echo "Installation completed successfully"
|
||||||
|
|||||||
@@ -12,11 +12,10 @@
|
|||||||
|
|
||||||
<h5>Quectel/EG25-G</h5>
|
<h5>Quectel/EG25-G</h5>
|
||||||
<p>FCC ID: XMR201903EG25G</p>
|
<p>FCC ID: XMR201903EG25G</p>
|
||||||
<p>
|
<p>This device complies with Part 15 of the FCC Rules.</p>
|
||||||
This device complies with Part 15 of the FCC Rules.
|
<p>Operation is subject to the following two conditions:</p>
|
||||||
Operation is subject to the following two conditions:
|
|
||||||
|
|
||||||
<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>
|
<p>(2) this device must accept any interference received, including interference that may cause undesired operation.</p>
|
||||||
|
|
||||||
The following test reports are subject to this declaration:
|
The following test reports are subject to this declaration:
|
||||||
|
|||||||
@@ -93,7 +93,8 @@ class Controls(ControlsExt):
|
|||||||
torque_params.frictionCoefficientFiltered)
|
torque_params.frictionCoefficientFiltered)
|
||||||
|
|
||||||
self.LaC.extension.update_model_v2(self.sm['modelV2'])
|
self.LaC.extension.update_model_v2(self.sm['modelV2'])
|
||||||
self.LaC.extension.update_lateral_lag(self.sm['liveDelay'].lateralDelay)
|
calculated_lag = self.LaC.extension.lagd_torqued_main(self.CP, self.sm['liveDelay'])
|
||||||
|
self.LaC.extension.update_lateral_lag(calculated_lag)
|
||||||
|
|
||||||
long_plan = self.sm['longitudinalPlan']
|
long_plan = self.sm['longitudinalPlan']
|
||||||
model_v2 = self.sm['modelV2']
|
model_v2 = self.sm['modelV2']
|
||||||
|
|||||||
@@ -94,9 +94,13 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
|||||||
|
|
||||||
def update(self, sm):
|
def update(self, sm):
|
||||||
self.mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc'
|
self.mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc'
|
||||||
|
if not self.mlsim:
|
||||||
|
self.mpc.mode = self.mode
|
||||||
LongitudinalPlannerSP.update(self, sm)
|
LongitudinalPlannerSP.update(self, sm)
|
||||||
if dec_mpc_mode := self.get_mpc_mode():
|
if dec_mpc_mode := self.get_mpc_mode():
|
||||||
self.mode = dec_mpc_mode
|
self.mode = dec_mpc_mode
|
||||||
|
if not self.mlsim:
|
||||||
|
self.mpc.mode = dec_mpc_mode
|
||||||
|
|
||||||
if len(sm['carControl'].orientationNED) == 3:
|
if len(sm['carControl'].orientationNED) == 3:
|
||||||
accel_coast = get_coast_accel(sm['carControl'].orientationNED[1])
|
accel_coast = get_coast_accel(sm['carControl'].orientationNED[1])
|
||||||
@@ -171,7 +175,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
|||||||
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
|
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
|
||||||
output_should_stop_e2e = sm['modelV2'].action.shouldStop
|
output_should_stop_e2e = sm['modelV2'].action.shouldStop
|
||||||
|
|
||||||
if self.mode == 'acc':
|
if self.mode == 'acc' or not self.mlsim:
|
||||||
output_a_target = output_a_target_mpc
|
output_a_target = output_a_target_mpc
|
||||||
self.output_should_stop = output_should_stop_mpc
|
self.output_should_stop = output_should_stop_mpc
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -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
|
from cereal import log, car
|
||||||
import cereal.messaging as messaging
|
import cereal.messaging as messaging
|
||||||
|
from openpilot.system.hardware import HARDWARE
|
||||||
from openpilot.common.conversions import Conversions as CV
|
from openpilot.common.conversions import Conversions as CV
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.common.realtime import config_realtime_process
|
from openpilot.common.realtime import config_realtime_process
|
||||||
@@ -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])
|
WIDE_FROM_DEVICE_EULER_INIT = np.array([0.0, 0.0, 0.0])
|
||||||
HEIGHT_INIT = np.array([1.22])
|
HEIGHT_INIT = np.array([1.22])
|
||||||
|
|
||||||
# These values are needed to accommodate the model frame in the narrow cam of the C3
|
# These values are needed to accommodate the model frame in the narrow cam
|
||||||
PITCH_LIMITS = np.array([-0.09074112085129739, 0.17])
|
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])
|
YAW_LIMITS = np.array([-0.06912048084718224, 0.06912048084718235])
|
||||||
DEBUG = os.getenv("DEBUG") is not None
|
DEBUG = os.getenv("DEBUG") is not None
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ class PointBuckets:
|
|||||||
total_points_valid = self.__len__() >= self.min_points_total
|
total_points_valid = self.__len__() >= self.min_points_total
|
||||||
return individual_buckets_valid and total_points_valid
|
return individual_buckets_valid and total_points_valid
|
||||||
|
|
||||||
|
def get_valid_percent(self) -> int:
|
||||||
|
total_points_perc = min(self.__len__() / self.min_points_total * 100, 100)
|
||||||
|
individual_buckets_perc = min(min(len(v) / min_pts * 100 for v, min_pts in
|
||||||
|
zip(self.buckets.values(), self.buckets_min_points.values(), strict=True)), 100)
|
||||||
|
return int((total_points_perc + individual_buckets_perc) / 2)
|
||||||
|
|
||||||
def is_calculable(self) -> bool:
|
def is_calculable(self) -> bool:
|
||||||
return all(len(v) > 0 for v in self.buckets.values())
|
return all(len(v) > 0 for v in self.buckets.values())
|
||||||
|
|
||||||
|
|||||||
@@ -229,6 +229,8 @@ class LateralLagEstimator:
|
|||||||
liveDelay.lateralDelayEstimateStd = 0.0
|
liveDelay.lateralDelayEstimateStd = 0.0
|
||||||
|
|
||||||
liveDelay.validBlocks = self.block_avg.valid_blocks
|
liveDelay.validBlocks = self.block_avg.valid_blocks
|
||||||
|
liveDelay.calPerc = min(100 * (self.block_avg.valid_blocks * self.block_size + self.block_avg.idx) //
|
||||||
|
(self.min_valid_block_count * self.block_size), 100)
|
||||||
if debug:
|
if debug:
|
||||||
liveDelay.points = self.block_avg.values.flatten().tolist()
|
liveDelay.points = self.block_avg.values.flatten().tolist()
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class TestLagd:
|
|||||||
assert np.allclose(msg.liveDelay.lateralDelay, estimator.initial_lag)
|
assert np.allclose(msg.liveDelay.lateralDelay, estimator.initial_lag)
|
||||||
assert np.allclose(msg.liveDelay.lateralDelayEstimate, estimator.initial_lag)
|
assert np.allclose(msg.liveDelay.lateralDelayEstimate, estimator.initial_lag)
|
||||||
assert msg.liveDelay.validBlocks == 0
|
assert msg.liveDelay.validBlocks == 0
|
||||||
|
assert msg.liveDelay.calPerc == 0
|
||||||
|
|
||||||
def test_estimator_basics(self, subtests):
|
def test_estimator_basics(self, subtests):
|
||||||
for lag_frames in range(5):
|
for lag_frames in range(5):
|
||||||
@@ -107,6 +108,7 @@ class TestLagd:
|
|||||||
assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
|
assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
|
||||||
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
|
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
|
||||||
assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED
|
assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED
|
||||||
|
assert msg.liveDelay.calPerc == 100
|
||||||
|
|
||||||
def test_disabled_estimator(self):
|
def test_disabled_estimator(self):
|
||||||
mocked_CP = car.CarParams(steerActuatorDelay=0.8)
|
mocked_CP = car.CarParams(steerActuatorDelay=0.8)
|
||||||
@@ -119,6 +121,7 @@ class TestLagd:
|
|||||||
assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
|
assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
|
||||||
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
|
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
|
||||||
assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED
|
assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED
|
||||||
|
assert msg.liveDelay.calPerc == 100
|
||||||
|
|
||||||
def test_estimator_masking(self):
|
def test_estimator_masking(self):
|
||||||
mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.8), random.randint(1, 19)
|
mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.8), random.randint(1, 19)
|
||||||
@@ -127,6 +130,7 @@ class TestLagd:
|
|||||||
msg = estimator.get_msg(True)
|
msg = estimator.get_msg(True)
|
||||||
assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
|
assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
|
||||||
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
|
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
|
||||||
|
assert msg.liveDelay.calPerc == 100
|
||||||
|
|
||||||
@pytest.mark.skipif(PC, reason="only on device")
|
@pytest.mark.skipif(PC, reason="only on device")
|
||||||
@pytest.mark.timeout(60)
|
@pytest.mark.timeout(60)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -11,6 +11,8 @@ from openpilot.common.filter_simple import FirstOrderFilter
|
|||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
from openpilot.selfdrive.locationd.helpers import PointBuckets, ParameterEstimator, PoseCalibrator, Pose
|
from openpilot.selfdrive.locationd.helpers import PointBuckets, ParameterEstimator, PoseCalibrator, Pose
|
||||||
|
|
||||||
|
from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle
|
||||||
|
|
||||||
HISTORY = 5 # secs
|
HISTORY = 5 # secs
|
||||||
POINTS_PER_BUCKET = 1500
|
POINTS_PER_BUCKET = 1500
|
||||||
MIN_POINTS_TOTAL = 4000
|
MIN_POINTS_TOTAL = 4000
|
||||||
@@ -49,8 +51,10 @@ class TorqueBuckets(PointBuckets):
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
class TorqueEstimator(ParameterEstimator):
|
class TorqueEstimator(ParameterEstimator, LagdToggle):
|
||||||
def __init__(self, CP, decimated=False, track_all_points=False):
|
def __init__(self, CP, decimated=False, track_all_points=False):
|
||||||
|
super().__init__()
|
||||||
|
self.CP = CP
|
||||||
self.hist_len = int(HISTORY / DT_MDL)
|
self.hist_len = int(HISTORY / DT_MDL)
|
||||||
self.lag = 0.0
|
self.lag = 0.0
|
||||||
self.track_all_points = track_all_points # for offline analysis, without max lateral accel or max steer torque filters
|
self.track_all_points = track_all_points # for offline analysis, without max lateral accel or max steer torque filters
|
||||||
@@ -176,7 +180,7 @@ class TorqueEstimator(ParameterEstimator):
|
|||||||
elif which == "liveCalibration":
|
elif which == "liveCalibration":
|
||||||
self.calibrator.feed_live_calib(msg)
|
self.calibrator.feed_live_calib(msg)
|
||||||
elif which == "liveDelay":
|
elif which == "liveDelay":
|
||||||
self.lag = msg.lateralDelay
|
self.lag = self.lagd_torqued_main(self.CP, msg)
|
||||||
# calculate lateral accel from past steering torque
|
# calculate lateral accel from past steering torque
|
||||||
elif which == "livePose":
|
elif which == "livePose":
|
||||||
if len(self.raw_points['steer_torque']) == self.hist_len:
|
if len(self.raw_points['steer_torque']) == self.hist_len:
|
||||||
@@ -233,6 +237,7 @@ class TorqueEstimator(ParameterEstimator):
|
|||||||
liveTorqueParameters.latAccelOffsetFiltered = float(self.filtered_params['latAccelOffset'].x)
|
liveTorqueParameters.latAccelOffsetFiltered = float(self.filtered_params['latAccelOffset'].x)
|
||||||
liveTorqueParameters.frictionCoefficientFiltered = float(self.filtered_params['frictionCoefficient'].x)
|
liveTorqueParameters.frictionCoefficientFiltered = float(self.filtered_params['frictionCoefficient'].x)
|
||||||
liveTorqueParameters.totalBucketPoints = len(self.filtered_points)
|
liveTorqueParameters.totalBucketPoints = len(self.filtered_points)
|
||||||
|
liveTorqueParameters.calPerc = self.filtered_points.get_valid_percent()
|
||||||
liveTorqueParameters.decay = self.decay
|
liveTorqueParameters.decay = self.decay
|
||||||
liveTorqueParameters.maxResets = self.resets
|
liveTorqueParameters.maxResets = self.resets
|
||||||
return msg
|
return msg
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ import subprocess
|
|||||||
from tinygrad import Device
|
from tinygrad import Device
|
||||||
|
|
||||||
# because tg doesn't support multi-process
|
# because tg doesn't support multi-process
|
||||||
devs = subprocess.check_output('python3 -c "from tinygrad import Device; print(list(Device.get_available_devices()))"', shell=True)
|
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:
|
if b"AMD" in devs:
|
||||||
del Device
|
del Device
|
||||||
print("USB GPU detected... building")
|
print("USB GPU detected... building")
|
||||||
|
|||||||
+26
-22
@@ -37,6 +37,8 @@ from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_
|
|||||||
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
||||||
from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext
|
from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext
|
||||||
|
|
||||||
|
from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle
|
||||||
|
|
||||||
|
|
||||||
PROCESS_NAME = "selfdrive.modeld.modeld"
|
PROCESS_NAME = "selfdrive.modeld.modeld"
|
||||||
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||||
@@ -86,10 +88,21 @@ class ModelState:
|
|||||||
prev_desire: np.ndarray # for tracking the rising edge of the pulse
|
prev_desire: np.ndarray # for tracking the rising edge of the pulse
|
||||||
|
|
||||||
def __init__(self, context: CLContext):
|
def __init__(self, context: CLContext):
|
||||||
self.frames = {
|
self.LAT_SMOOTH_SECONDS = 0.0
|
||||||
'input_imgs': DrivingModelFrame(context, ModelConstants.TEMPORAL_SKIP),
|
with open(VISION_METADATA_PATH, 'rb') as f:
|
||||||
'big_input_imgs': DrivingModelFrame(context, ModelConstants.TEMPORAL_SKIP)
|
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.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
|
||||||
|
|
||||||
self.full_features_buffer = np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32)
|
self.full_features_buffer = np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32)
|
||||||
@@ -106,18 +119,6 @@ class ModelState:
|
|||||||
'features_buffer': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32),
|
'features_buffer': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32),
|
||||||
}
|
}
|
||||||
|
|
||||||
with open(VISION_METADATA_PATH, 'rb') as f:
|
|
||||||
vision_metadata = pickle.load(f)
|
|
||||||
self.vision_input_shapes = vision_metadata['input_shapes']
|
|
||||||
self.vision_output_slices = vision_metadata['output_slices']
|
|
||||||
vision_output_size = vision_metadata['output_shapes']['outputs'][1]
|
|
||||||
|
|
||||||
with open(POLICY_METADATA_PATH, 'rb') as f:
|
|
||||||
policy_metadata = pickle.load(f)
|
|
||||||
self.policy_input_shapes = policy_metadata['input_shapes']
|
|
||||||
self.policy_output_slices = policy_metadata['output_slices']
|
|
||||||
policy_output_size = policy_metadata['output_shapes']['outputs'][1]
|
|
||||||
|
|
||||||
# img buffers are managed in openCL transform code
|
# img buffers are managed in openCL transform code
|
||||||
self.vision_inputs: dict[str, Tensor] = {}
|
self.vision_inputs: dict[str, Tensor] = {}
|
||||||
self.vision_output = np.zeros(vision_output_size, dtype=np.float32)
|
self.vision_output = np.zeros(vision_output_size, dtype=np.float32)
|
||||||
@@ -135,7 +136,7 @@ class ModelState:
|
|||||||
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
|
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
|
||||||
return parsed_model_outputs
|
return parsed_model_outputs
|
||||||
|
|
||||||
def run(self, 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:
|
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
|
||||||
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
||||||
inputs['desire'][0] = 0
|
inputs['desire'][0] = 0
|
||||||
@@ -148,8 +149,7 @@ class ModelState:
|
|||||||
|
|
||||||
self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention']
|
self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention']
|
||||||
self.numpy_inputs['lateral_control_params'][:] = inputs['lateral_control_params']
|
self.numpy_inputs['lateral_control_params'][:] = inputs['lateral_control_params']
|
||||||
imgs_cl = {'input_imgs': self.frames['input_imgs'].prepare(buf, transform.flatten()),
|
imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.vision_input_names}
|
||||||
'big_input_imgs': self.frames['big_input_imgs'].prepare(wbuf, transform_wide.flatten())}
|
|
||||||
|
|
||||||
if TICI and not USBGPU:
|
if TICI and not USBGPU:
|
||||||
# The imgs tensors are backed by opencl memory, only need init once
|
# The imgs tensors are backed by opencl memory, only need init once
|
||||||
@@ -254,6 +254,8 @@ def main(demo=False):
|
|||||||
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
|
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
|
||||||
cloudlog.info("modeld got CarParams: %s", CP.brand)
|
cloudlog.info("modeld got CarParams: %s", CP.brand)
|
||||||
|
|
||||||
|
modeld_lagd = LagdToggle()
|
||||||
|
|
||||||
# TODO this needs more thought, use .2s extra for now to estimate other delays
|
# TODO this needs more thought, use .2s extra for now to estimate other delays
|
||||||
# TODO Move smooth seconds to action function
|
# TODO Move smooth seconds to action function
|
||||||
long_delay = CP.longitudinalActuatorDelay + LONG_SMOOTH_SECONDS
|
long_delay = CP.longitudinalActuatorDelay + LONG_SMOOTH_SECONDS
|
||||||
@@ -299,7 +301,7 @@ def main(demo=False):
|
|||||||
is_rhd = sm["driverMonitoringState"].isRHD
|
is_rhd = sm["driverMonitoringState"].isRHD
|
||||||
frame_id = sm["roadCameraState"].frameId
|
frame_id = sm["roadCameraState"].frameId
|
||||||
v_ego = max(sm["carState"].vEgo, 0.)
|
v_ego = max(sm["carState"].vEgo, 0.)
|
||||||
lat_delay = sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS
|
lat_delay = modeld_lagd.lagd_main(CP, sm, model)
|
||||||
lateral_control_params = np.array([v_ego, lat_delay], dtype=np.float32)
|
lateral_control_params = np.array([v_ego, lat_delay], dtype=np.float32)
|
||||||
if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']:
|
if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']:
|
||||||
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
|
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
|
||||||
@@ -328,14 +330,16 @@ def main(demo=False):
|
|||||||
if prepare_only:
|
if prepare_only:
|
||||||
cloudlog.error(f"skipping model eval. Dropped {vipc_dropped_frames} frames")
|
cloudlog.error(f"skipping model eval. Dropped {vipc_dropped_frames} frames")
|
||||||
|
|
||||||
|
bufs = {name: buf_extra if 'big' in name else buf_main for name in model.vision_input_names}
|
||||||
|
transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.vision_input_names}
|
||||||
inputs:dict[str, np.ndarray] = {
|
inputs:dict[str, np.ndarray] = {
|
||||||
'desire': vec_desire,
|
'desire': vec_desire,
|
||||||
'traffic_convention': traffic_convention,
|
'traffic_convention': traffic_convention,
|
||||||
'lateral_control_params': lateral_control_params,
|
'lateral_control_params': lateral_control_params,
|
||||||
}
|
}
|
||||||
|
|
||||||
mt1 = time.perf_counter()
|
mt1 = time.perf_counter()
|
||||||
model_output = model.run(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()
|
mt2 = time.perf_counter()
|
||||||
model_execution_time = mt2 - mt1
|
model_execution_time = mt2 - mt1
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:98f0121ccb6f850077b04cc91bd33d370fc6cbdc2bd35f1ab55628a15a813f36
|
oid sha256:5f714fb38bcd44b5d9f44e3a8e1595e1dfdf7558f0eec3485cf3f2dbb6dc7d8d
|
||||||
size 15966721
|
size 15971805
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:897f80d0388250f99bba69b6a8434560cc0fd83157cbeb0bc134c67fe4e64624
|
oid sha256:3ac4867fbc618037e8d03143edbfeeae960f2025644b5dcf36c6665271b4f874
|
||||||
size 34882971
|
size 34883375
|
||||||
|
|||||||
@@ -585,11 +585,6 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
EventName.noGps: {
|
EventName.noGps: {
|
||||||
ET.PERMANENT: Alert(
|
|
||||||
"Poor GPS reception",
|
|
||||||
"Ensure device has a clear view of the sky",
|
|
||||||
AlertStatus.normal, AlertSize.mid,
|
|
||||||
Priority.LOWER, VisualAlert.none, AudibleAlert.none, .2, creation_delay=600.)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
EventName.tooDistracted: {
|
EventName.tooDistracted: {
|
||||||
|
|||||||
@@ -382,16 +382,16 @@ class SelfdriveD(CruiseHelper):
|
|||||||
if (planner_fcw or model_fcw) and not self.CP.notCar:
|
if (planner_fcw or model_fcw) and not self.CP.notCar:
|
||||||
self.events.add(EventName.fcw)
|
self.events.add(EventName.fcw)
|
||||||
|
|
||||||
|
# GPS checks
|
||||||
|
gps_ok = self.sm.recv_frame[self.gps_location_service] > 0 and (self.sm.frame - self.sm.recv_frame[self.gps_location_service]) * DT_CTRL < 2.0
|
||||||
|
if not gps_ok and self.sm['livePose'].inputsOK and (self.distance_traveled > 1500):
|
||||||
|
self.events.add(EventName.noGps)
|
||||||
|
if gps_ok:
|
||||||
|
self.distance_traveled = 0
|
||||||
|
self.distance_traveled += abs(CS.vEgo) * DT_CTRL
|
||||||
|
|
||||||
# TODO: fix simulator
|
# TODO: fix simulator
|
||||||
if not SIMULATION or REPLAY:
|
if not SIMULATION or REPLAY:
|
||||||
# Not show in first 1.5 km to allow for driving out of garage. This event shows after 5 minutes
|
|
||||||
gps_ok = self.sm.recv_frame[self.gps_location_service] > 0 and (self.sm.frame - self.sm.recv_frame[self.gps_location_service]) * DT_CTRL < 2.0
|
|
||||||
if not gps_ok and self.sm['livePose'].inputsOK and (self.distance_traveled > 1500):
|
|
||||||
self.events.add(EventName.noGps)
|
|
||||||
if gps_ok:
|
|
||||||
self.distance_traveled = 0
|
|
||||||
self.distance_traveled += abs(CS.vEgo) * DT_CTRL
|
|
||||||
|
|
||||||
if self.sm['modelV2'].frameDropPerc > 20:
|
if self.sm['modelV2'].frameDropPerc > 20:
|
||||||
self.events.add(EventName.modeldLagging)
|
self.events.add(EventName.modeldLagging)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
#!/usr/bin/env python3
|
||||||
import os
|
import os
|
||||||
|
import pickle
|
||||||
import sys
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -189,22 +190,44 @@ def model_replay(lr, frs):
|
|||||||
print("----------------- Model Timing -----------------")
|
print("----------------- Model Timing -----------------")
|
||||||
print("------------------------------------------------")
|
print("------------------------------------------------")
|
||||||
print(tabulate(rows, header, tablefmt="simple_grid", stralign="center", numalign="center", floatfmt=".4f"))
|
print(tabulate(rows, header, tablefmt="simple_grid", stralign="center", numalign="center", floatfmt=".4f"))
|
||||||
assert timings_ok
|
assert timings_ok or PC
|
||||||
|
|
||||||
return msgs
|
return msgs
|
||||||
|
|
||||||
|
|
||||||
|
def get_frames():
|
||||||
|
regen_cache = "--regen-cache" in sys.argv
|
||||||
|
frames_cache = '/tmp/model_replay_cache' if PC else '/data/model_replay_cache'
|
||||||
|
os.makedirs(frames_cache, exist_ok=True)
|
||||||
|
|
||||||
|
cache_name = f'{frames_cache}/{TEST_ROUTE}_{SEGMENT}_{START_FRAME}_{END_FRAME}.pkl'
|
||||||
|
if os.path.isfile(cache_name) and not regen_cache:
|
||||||
|
try:
|
||||||
|
print(f"Loading frames from cache {cache_name}")
|
||||||
|
return pickle.load(open(cache_name, "rb"))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to load frames from cache {cache_name}: {e}")
|
||||||
|
|
||||||
|
frs = {
|
||||||
|
'roadCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "fcamera.hevc"), pix_fmt='nv12', cache_size=END_FRAME - START_FRAME),
|
||||||
|
'driverCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "dcamera.hevc"), pix_fmt='nv12', cache_size=END_FRAME - START_FRAME),
|
||||||
|
'wideRoadCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "ecamera.hevc"), pix_fmt='nv12', cache_size=END_FRAME - START_FRAME),
|
||||||
|
}
|
||||||
|
for fr in frs.values():
|
||||||
|
for fidx in range(START_FRAME, END_FRAME):
|
||||||
|
fr.get(fidx)
|
||||||
|
fr.it = None
|
||||||
|
print(f"Dumping frame cache {cache_name}")
|
||||||
|
pickle.dump(frs, open(cache_name, "wb"))
|
||||||
|
return frs
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
update = "--update" in sys.argv or (os.getenv("GIT_BRANCH", "") == 'master')
|
update = "--update" in sys.argv or (os.getenv("GIT_BRANCH", "") == 'master')
|
||||||
replay_dir = os.path.dirname(os.path.abspath(__file__))
|
replay_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
# load logs
|
# load logs
|
||||||
lr = list(LogReader(get_url(TEST_ROUTE, SEGMENT, "rlog.zst")))
|
lr = list(LogReader(get_url(TEST_ROUTE, SEGMENT, "rlog.zst")))
|
||||||
frs = {
|
frs = get_frames()
|
||||||
'roadCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "fcamera.hevc"), readahead=True),
|
|
||||||
'driverCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "dcamera.hevc"), readahead=True),
|
|
||||||
'wideRoadCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "ecamera.hevc"), readahead=True)
|
|
||||||
}
|
|
||||||
|
|
||||||
log_msgs = []
|
log_msgs = []
|
||||||
# run replays
|
# run replays
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera
|
|||||||
from openpilot.selfdrive.test.process_replay.migration import migrate_all
|
from openpilot.selfdrive.test.process_replay.migration import migrate_all
|
||||||
from openpilot.selfdrive.test.process_replay.capture import ProcessOutputCapture
|
from openpilot.selfdrive.test.process_replay.capture import ProcessOutputCapture
|
||||||
from openpilot.tools.lib.logreader import LogIterable
|
from openpilot.tools.lib.logreader import LogIterable
|
||||||
from openpilot.tools.lib.framereader import BaseFrameReader
|
from openpilot.tools.lib.framereader import FrameReader
|
||||||
|
|
||||||
# Numpy gives different results based on CPU features after version 19
|
# Numpy gives different results based on CPU features after version 19
|
||||||
NUMPY_TOLERANCE = 1e-7
|
NUMPY_TOLERANCE = 1e-7
|
||||||
@@ -209,6 +209,7 @@ class ProcessContainer:
|
|||||||
streams_metas = available_streams(all_msgs)
|
streams_metas = available_streams(all_msgs)
|
||||||
for meta in streams_metas:
|
for meta in streams_metas:
|
||||||
if meta.camera_state in self.cfg.vision_pubs:
|
if meta.camera_state in self.cfg.vision_pubs:
|
||||||
|
assert frs[meta.camera_state].pix_fmt == 'nv12'
|
||||||
frame_size = (frs[meta.camera_state].w, frs[meta.camera_state].h)
|
frame_size = (frs[meta.camera_state].w, frs[meta.camera_state].h)
|
||||||
vipc_server.create_buffers(meta.stream, 2, *frame_size)
|
vipc_server.create_buffers(meta.stream, 2, *frame_size)
|
||||||
vipc_server.start_listener()
|
vipc_server.start_listener()
|
||||||
@@ -224,7 +225,7 @@ class ProcessContainer:
|
|||||||
|
|
||||||
def start(
|
def start(
|
||||||
self, params_config: dict[str, Any], environ_config: dict[str, Any],
|
self, params_config: dict[str, Any], environ_config: dict[str, Any],
|
||||||
all_msgs: LogIterable, frs: dict[str, BaseFrameReader] | None,
|
all_msgs: LogIterable, frs: dict[str, FrameReader] | None,
|
||||||
fingerprint: str | None, capture_output: bool
|
fingerprint: str | None, capture_output: bool
|
||||||
):
|
):
|
||||||
with self.prefix as p:
|
with self.prefix as p:
|
||||||
@@ -266,7 +267,7 @@ class ProcessContainer:
|
|||||||
self.prefix.clean_dirs()
|
self.prefix.clean_dirs()
|
||||||
self._clean_env()
|
self._clean_env()
|
||||||
|
|
||||||
def run_step(self, msg: capnp._DynamicStructReader, frs: dict[str, 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
|
assert self.rc and self.pm and self.sockets and self.process.proc
|
||||||
|
|
||||||
output_msgs = []
|
output_msgs = []
|
||||||
@@ -296,7 +297,7 @@ class ProcessContainer:
|
|||||||
camera_state = getattr(m, m.which())
|
camera_state = getattr(m, m.which())
|
||||||
camera_meta = meta_from_camera_state(m.which())
|
camera_meta = meta_from_camera_state(m.which())
|
||||||
assert frs is not None
|
assert frs is not None
|
||||||
img = frs[m.which()].get(camera_state.frameId, pix_fmt="nv12")[0]
|
img = frs[m.which()].get(camera_state.frameId)
|
||||||
self.vipc_server.send(camera_meta.stream, img.flatten().tobytes(),
|
self.vipc_server.send(camera_meta.stream, img.flatten().tobytes(),
|
||||||
camera_state.frameId, camera_state.timestampSof, camera_state.timestampEof)
|
camera_state.frameId, camera_state.timestampSof, camera_state.timestampEof)
|
||||||
self.msg_queue = []
|
self.msg_queue = []
|
||||||
@@ -655,7 +656,7 @@ def replay_process_with_name(name: str | Iterable[str], lr: LogIterable, *args,
|
|||||||
|
|
||||||
|
|
||||||
def replay_process(
|
def replay_process(
|
||||||
cfg: ProcessConfig | Iterable[ProcessConfig], lr: LogIterable, frs: dict[str, 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,
|
fingerprint: str = None, return_all_logs: bool = False, custom_params: dict[str, Any] = None,
|
||||||
captured_output_store: dict[str, dict[str, str]] = None, disable_progress: bool = False
|
captured_output_store: dict[str, dict[str, str]] = None, disable_progress: bool = False
|
||||||
) -> list[capnp._DynamicStructReader]:
|
) -> list[capnp._DynamicStructReader]:
|
||||||
@@ -683,7 +684,7 @@ def replay_process(
|
|||||||
|
|
||||||
|
|
||||||
def _replay_multi_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
|
custom_params: dict[str, Any] | None, captured_output_store: dict[str, dict[str, str]] | None, disable_progress: bool
|
||||||
) -> list[capnp._DynamicStructReader]:
|
) -> list[capnp._DynamicStructReader]:
|
||||||
if fingerprint is not None:
|
if fingerprint is not None:
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
9e2fe2942fbf77f24bccdbef15893831f9c0b390
|
f440c9e0469d32d350aa99ddaa8f44591a2ce690
|
||||||
@@ -3,40 +3,17 @@ import os
|
|||||||
import argparse
|
import argparse
|
||||||
import time
|
import time
|
||||||
import capnp
|
import capnp
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
|
|
||||||
from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, FAKEDATA, ProcessConfig, replay_process, get_process_config, \
|
from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, FAKEDATA, ProcessConfig, replay_process, get_process_config, \
|
||||||
check_openpilot_enabled, check_most_messages_valid, get_custom_params_from_lr
|
check_openpilot_enabled, check_most_messages_valid, get_custom_params_from_lr
|
||||||
from openpilot.selfdrive.test.process_replay.vision_meta import DRIVER_CAMERA_FRAME_SIZES
|
|
||||||
from openpilot.selfdrive.test.update_ci_routes import upload_route
|
from openpilot.selfdrive.test.update_ci_routes import upload_route
|
||||||
from openpilot.tools.lib.framereader import FrameReader, BaseFrameReader, FrameType
|
from openpilot.tools.lib.framereader import FrameReader
|
||||||
from openpilot.tools.lib.logreader import LogReader, LogIterable, save_log
|
from openpilot.tools.lib.logreader import LogReader, LogIterable, save_log
|
||||||
from openpilot.tools.lib.openpilotci import get_url
|
from openpilot.tools.lib.openpilotci import get_url
|
||||||
|
|
||||||
class DummyFrameReader(BaseFrameReader):
|
|
||||||
def __init__(self, w: int, h: int, frame_count: int, pix_val: int):
|
|
||||||
self.pix_val = pix_val
|
|
||||||
self.w, self.h = w, h
|
|
||||||
self.frame_count = frame_count
|
|
||||||
self.frame_type = FrameType.raw
|
|
||||||
|
|
||||||
def get(self, idx, count=1, pix_fmt="rgb24"):
|
|
||||||
if pix_fmt == "rgb24":
|
|
||||||
shape = (self.h, self.w, 3)
|
|
||||||
elif pix_fmt == "nv12" or pix_fmt == "yuv420p":
|
|
||||||
shape = (int((self.h * self.w) * 3 / 2),)
|
|
||||||
else:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
return [np.full(shape, self.pix_val, dtype=np.uint8) for _ in range(count)]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def zero_dcamera():
|
|
||||||
return DummyFrameReader(*DRIVER_CAMERA_FRAME_SIZES[("tici", "ar0231")], 1200, 0)
|
|
||||||
|
|
||||||
|
|
||||||
def regen_segment(
|
def regen_segment(
|
||||||
lr: LogIterable, frs: dict[str, Any] = None,
|
lr: LogIterable, frs: dict[str, Any] = None,
|
||||||
@@ -64,7 +41,7 @@ def setup_data_readers(
|
|||||||
frs['wideRoadCameraState'] = FrameReader(get_url(route, str(sidx), "ecamera.hevc"))
|
frs['wideRoadCameraState'] = FrameReader(get_url(route, str(sidx), "ecamera.hevc"))
|
||||||
if needs_driver_cam:
|
if needs_driver_cam:
|
||||||
if dummy_driver_cam:
|
if dummy_driver_cam:
|
||||||
frs['driverCameraState'] = DummyFrameReader.zero_dcamera()
|
frs['driverCameraState'] = FrameReader(get_url(route, str(sidx), "fcamera.hevc")) # Use fcam as dummy
|
||||||
else:
|
else:
|
||||||
device_type = next(str(msg.initData.deviceType) for msg in lr if msg.which() == "initData")
|
device_type = next(str(msg.initData.deviceType) for msg in lr if msg.which() == "initData")
|
||||||
assert device_type != "neo", "Driver camera not supported on neo segments. Use dummy dcamera."
|
assert device_type != "neo", "Driver camera not supported on neo segments. Use dummy dcamera."
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ from openpilot.tools.lib.logreader import LogReader, save_log
|
|||||||
IS_AZURE_TOKEN_DEFINED = os.getenv("AZURE_TOKEN")
|
IS_AZURE_TOKEN_DEFINED = os.getenv("AZURE_TOKEN")
|
||||||
|
|
||||||
source_segments = [
|
source_segments = [
|
||||||
("BODY", "937ccb7243511b65|2022-05-24--16-03-09--1"), # COMMA.COMMA_BODY
|
|
||||||
("HYUNDAI", "02c45f73a2e5c6e9|2021-01-01--19-08-22--1"), # HYUNDAI.HYUNDAI_SONATA
|
("HYUNDAI", "02c45f73a2e5c6e9|2021-01-01--19-08-22--1"), # HYUNDAI.HYUNDAI_SONATA
|
||||||
("HYUNDAI2", "d545129f3ca90f28|2022-11-07--20-43-08--3"), # HYUNDAI.HYUNDAI_KIA_EV6 (+ QCOM GPS)
|
("HYUNDAI2", "d545129f3ca90f28|2022-11-07--20-43-08--3"), # HYUNDAI.HYUNDAI_KIA_EV6 (+ QCOM GPS)
|
||||||
("TOYOTA", "0982d79ebb0de295|2021-01-04--17-13-21--13"), # TOYOTA.TOYOTA_PRIUS
|
("TOYOTA", "0982d79ebb0de295|2021-01-04--17-13-21--13"), # TOYOTA.TOYOTA_PRIUS
|
||||||
@@ -44,7 +43,6 @@ source_segments = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
segments = [
|
segments = [
|
||||||
("BODY", "regen2F3C7259F1B|2025-04-08--23-00-23--0"),
|
|
||||||
("HYUNDAI", "regenAA0FC4ED71E|2025-04-08--22-57-50--0"),
|
("HYUNDAI", "regenAA0FC4ED71E|2025-04-08--22-57-50--0"),
|
||||||
("HYUNDAI2", "regenAFB9780D823|2025-04-08--23-00-34--0"),
|
("HYUNDAI2", "regenAFB9780D823|2025-04-08--23-00-34--0"),
|
||||||
("TOYOTA", "regen218A4DCFAA1|2025-04-08--22-57-51--0"),
|
("TOYOTA", "regen218A4DCFAA1|2025-04-08--22-57-51--0"),
|
||||||
@@ -65,7 +63,7 @@ segments = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
# dashcamOnly makes don't need to be tested until a full port is done
|
# dashcamOnly makes don't need to be tested until a full port is done
|
||||||
excluded_interfaces = ["mock", "tesla"]
|
excluded_interfaces = ["mock", "body"]
|
||||||
|
|
||||||
BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/"
|
BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/"
|
||||||
REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit")
|
REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit")
|
||||||
@@ -253,7 +251,7 @@ if __name__ == "__main__":
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# to speed things up, we only test all segments on card
|
# to speed things up, we only test all segments on card
|
||||||
if cfg.proc_name != '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
|
continue
|
||||||
|
|
||||||
cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst")
|
cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from parameterized import parameterized
|
from parameterized import parameterized
|
||||||
|
|
||||||
from openpilot.selfdrive.test.process_replay.regen import regen_segment, 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.selfdrive.test.process_replay.process_replay import check_openpilot_enabled
|
||||||
from openpilot.tools.lib.openpilotci import get_url
|
from openpilot.tools.lib.openpilotci import get_url
|
||||||
from openpilot.tools.lib.logreader import LogReader
|
from openpilot.tools.lib.logreader import LogReader
|
||||||
@@ -18,7 +18,7 @@ def ci_setup_data_readers(route, sidx):
|
|||||||
lr = LogReader(get_url(route, sidx, "rlog.bz2"))
|
lr = LogReader(get_url(route, sidx, "rlog.bz2"))
|
||||||
frs = {
|
frs = {
|
||||||
'roadCameraState': FrameReader(get_url(route, sidx, "fcamera.hevc")),
|
'roadCameraState': FrameReader(get_url(route, sidx, "fcamera.hevc")),
|
||||||
'driverCameraState': DummyFrameReader.zero_dcamera()
|
'driverCameraState': FrameReader(get_url(route, sidx, "fcamera.hevc")),
|
||||||
}
|
}
|
||||||
if next((True for m in lr if m.which() == "wideRoadCameraState"), False):
|
if next((True for m in lr if m.which() == "wideRoadCameraState"), False):
|
||||||
frs["wideRoadCameraState"] = FrameReader(get_url(route, sidx, "ecamera.hevc"))
|
frs["wideRoadCameraState"] = FrameReader(get_url(route, sidx, "ecamera.hevc"))
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ if GetOption('extras'):
|
|||||||
obj = raylib_env.Object(f"installer/installers/installer_{name}.o", ["installer/installer.cc"], CPPDEFINES=d)
|
obj = raylib_env.Object(f"installer/installers/installer_{name}.o", ["installer/installer.cc"], CPPDEFINES=d)
|
||||||
f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter], LIBS=raylib_libs)
|
f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter], LIBS=raylib_libs)
|
||||||
# keep installers small
|
# keep installers small
|
||||||
assert f[0].get_size() < 1300*1e3, f[0].get_size()
|
assert f[0].get_size() < 1900*1e3, f[0].get_size()
|
||||||
|
|
||||||
# build watch3
|
# build watch3
|
||||||
if arch in ['x86_64', 'aarch64', 'Darwin'] or GetOption('extras'):
|
if arch in ['x86_64', 'aarch64', 'Darwin'] or GetOption('extras'):
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ from collections.abc import Callable
|
|||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.selfdrive.ui.widgets.offroad_alerts import UpdateAlert, OffroadAlert
|
from openpilot.selfdrive.ui.widgets.offroad_alerts import UpdateAlert, OffroadAlert
|
||||||
|
from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButton
|
||||||
|
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
|
||||||
|
from openpilot.selfdrive.ui.widgets.setup import SetupWidget
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.lib.label import gui_label
|
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR
|
||||||
|
from openpilot.system.ui.lib.widget import Widget
|
||||||
|
|
||||||
HEADER_HEIGHT = 80
|
HEADER_HEIGHT = 80
|
||||||
HEAD_BUTTON_FONT_SIZE = 40
|
HEAD_BUTTON_FONT_SIZE = 40
|
||||||
@@ -25,8 +27,9 @@ class HomeLayoutState(IntEnum):
|
|||||||
ALERTS = 2
|
ALERTS = 2
|
||||||
|
|
||||||
|
|
||||||
class HomeLayout:
|
class HomeLayout(Widget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
self.params = Params()
|
self.params = Params()
|
||||||
|
|
||||||
self.update_alert = UpdateAlert()
|
self.update_alert = UpdateAlert()
|
||||||
@@ -47,6 +50,10 @@ class HomeLayout:
|
|||||||
self.update_notif_rect = rl.Rectangle(0, 0, 200, HEADER_HEIGHT - 10)
|
self.update_notif_rect = rl.Rectangle(0, 0, 200, HEADER_HEIGHT - 10)
|
||||||
self.alert_notif_rect = rl.Rectangle(0, 0, 220, HEADER_HEIGHT - 10)
|
self.alert_notif_rect = rl.Rectangle(0, 0, 220, HEADER_HEIGHT - 10)
|
||||||
|
|
||||||
|
self._prime_widget = PrimeWidget()
|
||||||
|
self._setup_widget = SetupWidget()
|
||||||
|
|
||||||
|
self._exp_mode_button = ExperimentalModeButton()
|
||||||
self._setup_callbacks()
|
self._setup_callbacks()
|
||||||
|
|
||||||
def _setup_callbacks(self):
|
def _setup_callbacks(self):
|
||||||
@@ -59,9 +66,7 @@ class HomeLayout:
|
|||||||
def _set_state(self, state: HomeLayoutState):
|
def _set_state(self, state: HomeLayoutState):
|
||||||
self.current_state = state
|
self.current_state = state
|
||||||
|
|
||||||
def render(self, rect: rl.Rectangle):
|
def _render(self, rect: rl.Rectangle):
|
||||||
self._update_layout_rects(rect)
|
|
||||||
|
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
if current_time - self.last_refresh >= REFRESH_INTERVAL:
|
if current_time - self.last_refresh >= REFRESH_INTERVAL:
|
||||||
self._refresh()
|
self._refresh()
|
||||||
@@ -78,16 +83,16 @@ class HomeLayout:
|
|||||||
elif self.current_state == HomeLayoutState.ALERTS:
|
elif self.current_state == HomeLayoutState.ALERTS:
|
||||||
self._render_alerts_view()
|
self._render_alerts_view()
|
||||||
|
|
||||||
def _update_layout_rects(self, rect: rl.Rectangle):
|
def _update_layout_rects(self):
|
||||||
self.header_rect = rl.Rectangle(
|
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_y = self._rect.y + CONTENT_MARGIN + HEADER_HEIGHT + SPACING
|
||||||
content_height = rect.height - CONTENT_MARGIN - HEADER_HEIGHT - SPACING - CONTENT_MARGIN
|
content_height = self._rect.height - CONTENT_MARGIN - HEADER_HEIGHT - SPACING - CONTENT_MARGIN
|
||||||
|
|
||||||
self.content_rect = rl.Rectangle(
|
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
|
left_width = self.content_rect.width - RIGHT_COLUMN_WIDTH - SPACING
|
||||||
@@ -170,28 +175,25 @@ class HomeLayout:
|
|||||||
self.offroad_alert.render(self.content_rect)
|
self.offroad_alert.render(self.content_rect)
|
||||||
|
|
||||||
def _render_left_column(self):
|
def _render_left_column(self):
|
||||||
rl.draw_rectangle_rounded(self.left_column_rect, 0.02, 10, PRIME_BG_COLOR)
|
self._prime_widget.render(self.left_column_rect)
|
||||||
gui_label(self.left_column_rect, "Prime Widget", 48, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
|
||||||
|
|
||||||
def _render_right_column(self):
|
def _render_right_column(self):
|
||||||
widget_height = (self.right_column_rect.height - SPACING) // 2
|
exp_height = 125
|
||||||
|
|
||||||
exp_rect = rl.Rectangle(
|
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)
|
self._exp_mode_button.render(exp_rect)
|
||||||
gui_label(exp_rect, "Experimental Mode", 36, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
|
||||||
|
|
||||||
setup_rect = rl.Rectangle(
|
setup_rect = rl.Rectangle(
|
||||||
self.right_column_rect.x,
|
self.right_column_rect.x,
|
||||||
self.right_column_rect.y + widget_height + SPACING,
|
self.right_column_rect.y + exp_height + SPACING,
|
||||||
self.right_column_rect.width,
|
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)
|
self._setup_widget.render(setup_rect)
|
||||||
gui_label(setup_rect, "Setup", 36, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
|
||||||
|
|
||||||
def _refresh(self):
|
def _refresh(self):
|
||||||
|
# TODO: implement _update_state with a timer
|
||||||
self.update_available = self.update_alert.refresh()
|
self.update_available = self.update_alert.refresh()
|
||||||
self.alert_count = self.offroad_alert.refresh()
|
self.alert_count = self.offroad_alert.refresh()
|
||||||
self._update_state_priority(self.update_available, self.alert_count > 0)
|
self._update_state_priority(self.update_available, self.alert_count > 0)
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import pyray as rl
|
import pyray as rl
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
import cereal.messaging as messaging
|
||||||
from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH
|
from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH
|
||||||
from openpilot.selfdrive.ui.layouts.home import HomeLayout
|
from openpilot.selfdrive.ui.layouts.home import HomeLayout
|
||||||
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout
|
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
||||||
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
|
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
|
||||||
|
from openpilot.system.ui.lib.widget import Widget
|
||||||
|
|
||||||
|
|
||||||
class MainState(IntEnum):
|
class MainState(IntEnum):
|
||||||
@@ -13,14 +15,15 @@ class MainState(IntEnum):
|
|||||||
ONROAD = 2
|
ONROAD = 2
|
||||||
|
|
||||||
|
|
||||||
class MainLayout:
|
class MainLayout(Widget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self._pm = messaging.PubMaster(['userFlag'])
|
||||||
|
|
||||||
self._sidebar = Sidebar()
|
self._sidebar = Sidebar()
|
||||||
self._sidebar_visible = True
|
|
||||||
self._current_mode = MainState.HOME
|
self._current_mode = MainState.HOME
|
||||||
self._prev_onroad = False
|
self._prev_onroad = False
|
||||||
self._window_rect = None
|
|
||||||
self._current_callback: callable | None = None
|
|
||||||
|
|
||||||
# Initialize layouts
|
# Initialize layouts
|
||||||
self._layouts = {MainState.HOME: HomeLayout(), MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()}
|
self._layouts = {MainState.HOME: HomeLayout(), MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()}
|
||||||
@@ -31,32 +34,23 @@ class MainLayout:
|
|||||||
# Set callbacks
|
# Set callbacks
|
||||||
self._setup_callbacks()
|
self._setup_callbacks()
|
||||||
|
|
||||||
def render(self, rect):
|
def _render(self, _):
|
||||||
self._current_callback = None
|
|
||||||
|
|
||||||
self._update_layout_rects(rect)
|
|
||||||
self._handle_onroad_transition()
|
self._handle_onroad_transition()
|
||||||
self._render_main_content()
|
self._render_main_content()
|
||||||
self._handle_input()
|
|
||||||
|
|
||||||
if self._current_callback:
|
|
||||||
self._current_callback()
|
|
||||||
|
|
||||||
def _setup_callbacks(self):
|
def _setup_callbacks(self):
|
||||||
self._sidebar.set_callbacks(
|
self._sidebar.set_callbacks(on_settings=self._on_settings_clicked,
|
||||||
on_settings=lambda: setattr(self, '_current_callback', self._on_settings_clicked),
|
on_flag=self._on_flag_clicked)
|
||||||
on_flag=lambda: setattr(self, '_current_callback', 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.SETTINGS].set_callbacks(
|
self._layouts[MainState.ONROAD].set_callbacks(on_click=self._on_onroad_clicked)
|
||||||
on_close=lambda: setattr(self, '_current_callback', self._set_mode_for_state)
|
device.add_interactive_timeout_callback(self._set_mode_for_state)
|
||||||
)
|
|
||||||
|
|
||||||
def _update_layout_rects(self, rect):
|
def _update_layout_rects(self):
|
||||||
self._window_rect = rect
|
self._sidebar_rect = rl.Rectangle(self._rect.x, self._rect.y, SIDEBAR_WIDTH, self._rect.height)
|
||||||
self._sidebar_rect = rl.Rectangle(rect.x, rect.y, SIDEBAR_WIDTH, rect.height)
|
|
||||||
|
|
||||||
x_offset = SIDEBAR_WIDTH if self._sidebar_visible else 0
|
x_offset = SIDEBAR_WIDTH if self._sidebar.is_visible else 0
|
||||||
self._content_rect = rl.Rectangle(rect.y + x_offset, rect.y, rect.width - x_offset, rect.height)
|
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):
|
def _handle_onroad_transition(self):
|
||||||
if ui_state.started != self._prev_onroad:
|
if ui_state.started != self._prev_onroad:
|
||||||
@@ -66,31 +60,34 @@ class MainLayout:
|
|||||||
|
|
||||||
def _set_mode_for_state(self):
|
def _set_mode_for_state(self):
|
||||||
if ui_state.started:
|
if ui_state.started:
|
||||||
|
# Don't hide sidebar from interactive timeout
|
||||||
|
if self._current_mode != MainState.ONROAD:
|
||||||
|
self._sidebar.set_visible(False)
|
||||||
self._current_mode = MainState.ONROAD
|
self._current_mode = MainState.ONROAD
|
||||||
self._sidebar_visible = False
|
|
||||||
else:
|
else:
|
||||||
self._current_mode = MainState.HOME
|
self._current_mode = MainState.HOME
|
||||||
self._sidebar_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):
|
def _on_settings_clicked(self):
|
||||||
self._current_mode = MainState.SETTINGS
|
self.open_settings(PanelType.DEVICE)
|
||||||
self._sidebar_visible = False
|
|
||||||
|
|
||||||
def _on_flag_clicked(self):
|
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):
|
def _render_main_content(self):
|
||||||
# Render sidebar
|
# Render sidebar
|
||||||
if self._sidebar_visible:
|
if self._sidebar.is_visible:
|
||||||
self._sidebar.render(self._sidebar_rect)
|
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)
|
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
|
import pyray as rl
|
||||||
|
from openpilot.system.ui.lib.widget import Widget
|
||||||
from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper
|
from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper
|
||||||
from openpilot.system.ui.widgets.network import WifiManagerUI
|
from openpilot.system.ui.widgets.network import WifiManagerUI
|
||||||
|
|
||||||
|
|
||||||
class NetworkLayout:
|
class NetworkLayout(Widget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
self.wifi_manager = WifiManagerWrapper()
|
self.wifi_manager = WifiManagerWrapper()
|
||||||
self.wifi_ui = WifiManagerUI(self.wifi_manager)
|
self.wifi_ui = WifiManagerUI(self.wifi_manager)
|
||||||
|
|
||||||
def render(self, rect: rl.Rectangle):
|
def _render(self, rect: rl.Rectangle):
|
||||||
self.wifi_ui.render(rect)
|
self.wifi_ui.render(rect)
|
||||||
|
|
||||||
@property
|
|
||||||
def require_full_screen(self):
|
|
||||||
return self.wifi_ui.require_full_screen
|
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.wifi_manager.shutdown()
|
self.wifi_manager.shutdown()
|
||||||
|
|||||||
@@ -1,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.common.params import Params
|
||||||
|
from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item
|
||||||
|
|
||||||
# Description constants
|
# Description constants
|
||||||
DESCRIPTIONS = {
|
DESCRIPTIONS = {
|
||||||
@@ -8,11 +11,16 @@ DESCRIPTIONS = {
|
|||||||
"See https://docs.comma.ai/how-to/connect-to-comma for more info."
|
"See https://docs.comma.ai/how-to/connect-to-comma for more info."
|
||||||
),
|
),
|
||||||
'joystick_debug_mode': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
|
'joystick_debug_mode': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
|
||||||
|
'ssh_key': (
|
||||||
|
"Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username " +
|
||||||
|
"other than your own. A comma employee will NEVER ask you to add their GitHub username."
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeveloperLayout:
|
class DeveloperLayout(Widget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
self._params = Params()
|
self._params = Params()
|
||||||
items = [
|
items = [
|
||||||
toggle_item(
|
toggle_item(
|
||||||
@@ -21,6 +29,7 @@ class DeveloperLayout:
|
|||||||
initial_state=self._params.get_bool("AdbEnabled"),
|
initial_state=self._params.get_bool("AdbEnabled"),
|
||||||
callback=self._on_enable_adb,
|
callback=self._on_enable_adb,
|
||||||
),
|
),
|
||||||
|
ssh_key_item("SSH Key", description=DESCRIPTIONS["ssh_key"]),
|
||||||
toggle_item(
|
toggle_item(
|
||||||
"Joystick Debug Mode",
|
"Joystick Debug Mode",
|
||||||
description=DESCRIPTIONS["joystick_debug_mode"],
|
description=DESCRIPTIONS["joystick_debug_mode"],
|
||||||
@@ -41,10 +50,10 @@ class DeveloperLayout:
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
self._list_widget = ListView(items)
|
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||||
|
|
||||||
def render(self, rect):
|
def _render(self, rect):
|
||||||
self._list_widget.render(rect)
|
self._scroller.render(rect)
|
||||||
|
|
||||||
def _on_enable_adb(self): pass
|
def _on_enable_adb(self): pass
|
||||||
def _on_joystick_debug_mode(self): pass
|
def _on_joystick_debug_mode(self): pass
|
||||||
|
|||||||
@@ -1,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.common.params import Params
|
||||||
|
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog
|
||||||
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.system.hardware import TICI
|
from openpilot.system.hardware import TICI
|
||||||
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
|
from openpilot.system.ui.lib.list_view import text_item, button_item, dual_button_item
|
||||||
|
from openpilot.system.ui.lib.scroller import Scroller
|
||||||
|
from openpilot.system.ui.lib.widget import Widget, DialogResult
|
||||||
|
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
|
||||||
|
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||||
|
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog, alert_dialog
|
||||||
|
from openpilot.system.ui.widgets.html_render import HtmlRenderer
|
||||||
|
|
||||||
# Description constants
|
# Description constants
|
||||||
DESCRIPTIONS = {
|
DESCRIPTIONS = {
|
||||||
'pair_device': "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.",
|
'pair_device': "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.",
|
||||||
'driver_camera': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
|
'driver_camera': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)",
|
||||||
'reset_calibration': (
|
'reset_calibration': (
|
||||||
"openpilot requires the device to be mounted within 4° left or right and within 5° " +
|
"openpilot requires the device to be mounted within 4° left or right and within 5° " +
|
||||||
"up or 9° down. openpilot is continuously calibrating, resetting is rarely required."
|
"up or 9° down. openpilot is continuously calibrating, resetting is rarely required."
|
||||||
),
|
),
|
||||||
'review_guide': "Review the rules, features, and limitations of openpilot",
|
'review_guide': "Review the rules, features, and limitations of openpilot",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceLayout:
|
class DeviceLayout(Widget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
params = Params()
|
super().__init__()
|
||||||
dongle_id = params.get("DongleId", encoding="utf-8") or "N/A"
|
|
||||||
serial = params.get("HardwareSerial") or "N/A"
|
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 = [
|
items = [
|
||||||
text_item("Dongle ID", dongle_id),
|
text_item("Dongle ID", dongle_id),
|
||||||
text_item("Serial", serial),
|
text_item("Serial", serial),
|
||||||
button_item("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], self._on_pair_device),
|
button_item("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], callback=self._pair_device),
|
||||||
button_item("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], self._on_driver_camera),
|
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'], self._on_reset_calibration),
|
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("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:
|
def _render(self, rect):
|
||||||
items.append(button_item("Regulatory", "VIEW", callback=self._on_regulatory))
|
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):
|
def _handle_language_selection(self, result: int):
|
||||||
self._list_widget.render(rect)
|
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_review_training_guide(self): pass
|
||||||
def _on_regulatory(self): pass
|
|
||||||
def _on_change_language(self): pass
|
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import pyray as rl
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from openpilot.common.api import Api, api_get
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.common.swaglog import cloudlog
|
||||||
|
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
|
||||||
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
|
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||||
|
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||||
|
from openpilot.system.ui.lib.widget import Widget
|
||||||
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
|
|
||||||
|
|
||||||
|
TITLE = "Firehose Mode"
|
||||||
|
DESCRIPTION = (
|
||||||
|
"openpilot learns to drive by watching humans, like you, drive.\n\n"
|
||||||
|
+ "Firehose Mode allows you to maximize your training data uploads to improve "
|
||||||
|
+ "openpilot's driving models. More data means bigger models, which means better Experimental Mode."
|
||||||
|
)
|
||||||
|
INSTRUCTIONS = (
|
||||||
|
"For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n"
|
||||||
|
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n"
|
||||||
|
+ "Frequently Asked Questions\n\n"
|
||||||
|
+ "Does it matter how or where I drive? Nope, just drive as you normally would.\n\n"
|
||||||
|
+ "Do all of my segments get pulled in Firehose Mode? No, we selectively pull a subset of your segments.\n\n"
|
||||||
|
+ "What's a good USB-C adapter? Any fast phone or laptop charger should be fine.\n\n"
|
||||||
|
+ "Does it matter which software I run? Yes, only upstream openpilot (and particular forks) are able to be used for training."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FirehoseLayout(Widget):
|
||||||
|
PARAM_KEY = "ApiCache_FirehoseStats"
|
||||||
|
GREEN = rl.Color(46, 204, 113, 255)
|
||||||
|
RED = rl.Color(231, 76, 60, 255)
|
||||||
|
GRAY = rl.Color(68, 68, 68, 255)
|
||||||
|
LIGHT_GRAY = rl.Color(228, 228, 228, 255)
|
||||||
|
UPDATE_INTERVAL = 30 # seconds
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.params = Params()
|
||||||
|
self.segment_count = self._get_segment_count()
|
||||||
|
self.scroll_panel = GuiScrollPanel()
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
self.update_thread = threading.Thread(target=self._update_loop, daemon=True)
|
||||||
|
self.update_thread.start()
|
||||||
|
self.last_update_time = 0
|
||||||
|
|
||||||
|
def _get_segment_count(self) -> int:
|
||||||
|
stats = self.params.get(self.PARAM_KEY, encoding='utf8')
|
||||||
|
if not stats:
|
||||||
|
return 0
|
||||||
|
try:
|
||||||
|
return int(json.loads(stats).get("firehose", 0))
|
||||||
|
except Exception:
|
||||||
|
cloudlog.exception(f"Failed to decode firehose stats: {stats}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.running = False
|
||||||
|
if self.update_thread and self.update_thread.is_alive():
|
||||||
|
self.update_thread.join(timeout=1.0)
|
||||||
|
|
||||||
|
def _render(self, rect: rl.Rectangle):
|
||||||
|
# Calculate content dimensions
|
||||||
|
content_width = rect.width - 80
|
||||||
|
content_height = self._calculate_content_height(int(content_width))
|
||||||
|
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
|
||||||
|
|
||||||
|
# Handle scrolling and render with clipping
|
||||||
|
scroll_offset = self.scroll_panel.handle_scroll(rect, content_rect)
|
||||||
|
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
||||||
|
self._render_content(rect, scroll_offset)
|
||||||
|
rl.end_scissor_mode()
|
||||||
|
|
||||||
|
def _calculate_content_height(self, content_width: int) -> int:
|
||||||
|
height = 80 # Top margin
|
||||||
|
|
||||||
|
# Title
|
||||||
|
height += 100 + 40
|
||||||
|
|
||||||
|
# Description
|
||||||
|
desc_font = gui_app.font(FontWeight.NORMAL)
|
||||||
|
desc_lines = wrap_text(desc_font, DESCRIPTION, 45, content_width)
|
||||||
|
height += len(desc_lines) * 45 + 40
|
||||||
|
|
||||||
|
# Status section
|
||||||
|
height += 32 # Separator
|
||||||
|
status_text, _ = self._get_status()
|
||||||
|
status_lines = wrap_text(gui_app.font(FontWeight.BOLD), status_text, 60, content_width)
|
||||||
|
height += len(status_lines) * 60 + 20
|
||||||
|
|
||||||
|
# Contribution count (if available)
|
||||||
|
if self.segment_count > 0:
|
||||||
|
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
|
||||||
|
contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 52, content_width)
|
||||||
|
height += len(contrib_lines) * 52 + 20
|
||||||
|
|
||||||
|
# Instructions section
|
||||||
|
height += 32 # Separator
|
||||||
|
inst_lines = wrap_text(gui_app.font(FontWeight.NORMAL), INSTRUCTIONS, 40, content_width)
|
||||||
|
height += len(inst_lines) * 40 + 40 # Bottom margin
|
||||||
|
|
||||||
|
return height
|
||||||
|
|
||||||
|
def _render_content(self, rect: rl.Rectangle, scroll_offset: rl.Vector2):
|
||||||
|
x = int(rect.x + 40)
|
||||||
|
y = int(rect.y + 40 + scroll_offset.y)
|
||||||
|
w = int(rect.width - 80)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title_font = gui_app.font(FontWeight.MEDIUM)
|
||||||
|
rl.draw_text_ex(title_font, TITLE, rl.Vector2(x, y), 100, 0, rl.WHITE)
|
||||||
|
y += 140
|
||||||
|
|
||||||
|
# Description
|
||||||
|
y = self._draw_wrapped_text(x, y, w, DESCRIPTION, gui_app.font(FontWeight.NORMAL), 45, rl.WHITE)
|
||||||
|
y += 40
|
||||||
|
|
||||||
|
# Separator
|
||||||
|
rl.draw_rectangle(x, y, w, 2, self.GRAY)
|
||||||
|
y += 30
|
||||||
|
|
||||||
|
# Status
|
||||||
|
status_text, status_color = self._get_status()
|
||||||
|
y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color)
|
||||||
|
y += 20
|
||||||
|
|
||||||
|
# Contribution count (if available)
|
||||||
|
if self.segment_count > 0:
|
||||||
|
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
|
||||||
|
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
|
||||||
|
y += 20
|
||||||
|
|
||||||
|
# Separator
|
||||||
|
rl.draw_rectangle(x, y, w, 2, self.GRAY)
|
||||||
|
y += 30
|
||||||
|
|
||||||
|
# Instructions
|
||||||
|
self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
|
||||||
|
|
||||||
|
def _draw_wrapped_text(self, x, y, width, text, font, size, color):
|
||||||
|
wrapped = wrap_text(font, text, size, width)
|
||||||
|
for line in wrapped:
|
||||||
|
rl.draw_text_ex(font, line, rl.Vector2(x, y), size, 0, color)
|
||||||
|
y += size
|
||||||
|
return y
|
||||||
|
|
||||||
|
def _get_status(self) -> tuple[str, rl.Color]:
|
||||||
|
network_type = ui_state.sm["deviceState"].networkType
|
||||||
|
network_metered = ui_state.sm["deviceState"].networkMetered
|
||||||
|
|
||||||
|
if not network_metered and network_type != 0: # Not metered and connected
|
||||||
|
return "ACTIVE", self.GREEN
|
||||||
|
else:
|
||||||
|
return "INACTIVE: connect to an unmetered network", self.RED
|
||||||
|
|
||||||
|
def _fetch_firehose_stats(self):
|
||||||
|
try:
|
||||||
|
dongle_id = self.params.get("DongleId", encoding='utf8')
|
||||||
|
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
|
||||||
|
return
|
||||||
|
identity_token = Api(dongle_id).get_token()
|
||||||
|
response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token)
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
self.segment_count = data.get("firehose", 0)
|
||||||
|
self.params.put(self.PARAM_KEY, json.dumps(data))
|
||||||
|
except Exception as e:
|
||||||
|
cloudlog.error(f"Failed to fetch firehose stats: {e}")
|
||||||
|
|
||||||
|
def _update_loop(self):
|
||||||
|
while self.running:
|
||||||
|
if not ui_state.started:
|
||||||
|
self._fetch_firehose_stats()
|
||||||
|
time.sleep(self.UPDATE_INTERVAL)
|
||||||
@@ -2,15 +2,15 @@ import pyray as rl
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from openpilot.common.params import Params
|
|
||||||
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
|
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
|
||||||
from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
|
from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
|
||||||
|
from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
|
||||||
from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
|
from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
|
||||||
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
|
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.label import gui_text_box
|
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.selfdrive.ui.layouts.network import NetworkLayout
|
from openpilot.selfdrive.ui.layouts.network import NetworkLayout
|
||||||
|
from openpilot.system.ui.lib.widget import Widget
|
||||||
|
|
||||||
# Import individual panels
|
# Import individual panels
|
||||||
|
|
||||||
@@ -18,9 +18,8 @@ SETTINGS_CLOSE_TEXT = "X"
|
|||||||
# Constants
|
# Constants
|
||||||
SIDEBAR_WIDTH = 500
|
SIDEBAR_WIDTH = 500
|
||||||
CLOSE_BTN_SIZE = 200
|
CLOSE_BTN_SIZE = 200
|
||||||
NAV_BTN_HEIGHT = 80
|
NAV_BTN_HEIGHT = 110
|
||||||
PANEL_MARGIN = 50
|
PANEL_MARGIN = 50
|
||||||
SCROLL_SPEED = 30
|
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
SIDEBAR_COLOR = rl.BLACK
|
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)
|
CLOSE_BTN_PRESSED = rl.Color(59, 59, 59, 255)
|
||||||
TEXT_NORMAL = rl.Color(128, 128, 128, 255)
|
TEXT_NORMAL = rl.Color(128, 128, 128, 255)
|
||||||
TEXT_SELECTED = rl.Color(255, 255, 255, 255)
|
TEXT_SELECTED = rl.Color(255, 255, 255, 255)
|
||||||
TEXT_PRESSED = rl.Color(173, 173, 173, 255)
|
|
||||||
|
|
||||||
|
|
||||||
class PanelType(IntEnum):
|
class PanelType(IntEnum):
|
||||||
@@ -45,23 +43,22 @@ class PanelType(IntEnum):
|
|||||||
class PanelInfo:
|
class PanelInfo:
|
||||||
name: str
|
name: str
|
||||||
instance: object
|
instance: object
|
||||||
button_rect: rl.Rectangle
|
button_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
class SettingsLayout:
|
class SettingsLayout(Widget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._params = Params()
|
super().__init__()
|
||||||
self._current_panel = PanelType.DEVICE
|
self._current_panel = PanelType.DEVICE
|
||||||
self._max_scroll = 0.0
|
|
||||||
|
|
||||||
# Panel configuration
|
# Panel configuration
|
||||||
self._panels = {
|
self._panels = {
|
||||||
PanelType.DEVICE: PanelInfo("Device", DeviceLayout(), rl.Rectangle(0, 0, 0, 0)),
|
PanelType.DEVICE: PanelInfo("Device", DeviceLayout()),
|
||||||
PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout(), rl.Rectangle(0, 0, 0, 0)),
|
PanelType.NETWORK: PanelInfo("Network", NetworkLayout()),
|
||||||
PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout(), rl.Rectangle(0, 0, 0, 0)),
|
PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout()),
|
||||||
PanelType.FIREHOSE: PanelInfo("Firehose", None, rl.Rectangle(0, 0, 0, 0)),
|
PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout()),
|
||||||
PanelType.NETWORK: PanelInfo("Network", NetworkLayout(), rl.Rectangle(0, 0, 0, 0)),
|
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout()),
|
||||||
PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayout(), rl.Rectangle(0, 0, 0, 0)),
|
PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayout()),
|
||||||
}
|
}
|
||||||
|
|
||||||
self._font_medium = gui_app.font(FontWeight.MEDIUM)
|
self._font_medium = gui_app.font(FontWeight.MEDIUM)
|
||||||
@@ -73,7 +70,7 @@ class SettingsLayout:
|
|||||||
def set_callbacks(self, on_close: Callable):
|
def set_callbacks(self, on_close: Callable):
|
||||||
self._close_callback = on_close
|
self._close_callback = on_close
|
||||||
|
|
||||||
def render(self, rect: rl.Rectangle):
|
def _render(self, rect: rl.Rectangle):
|
||||||
# Calculate layout
|
# Calculate layout
|
||||||
sidebar_rect = rl.Rectangle(rect.x, rect.y, SIDEBAR_WIDTH, rect.height)
|
sidebar_rect = rl.Rectangle(rect.x, rect.y, SIDEBAR_WIDTH, rect.height)
|
||||||
panel_rect = rl.Rectangle(rect.x + SIDEBAR_WIDTH, rect.y, rect.width - SIDEBAR_WIDTH, rect.height)
|
panel_rect = rl.Rectangle(rect.x + SIDEBAR_WIDTH, rect.y, rect.width - SIDEBAR_WIDTH, rect.height)
|
||||||
@@ -82,9 +79,6 @@ class SettingsLayout:
|
|||||||
self._draw_sidebar(sidebar_rect)
|
self._draw_sidebar(sidebar_rect)
|
||||||
self._draw_current_panel(panel_rect)
|
self._draw_current_panel(panel_rect)
|
||||||
|
|
||||||
if rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
|
||||||
self.handle_mouse_release(rl.get_mouse_position())
|
|
||||||
|
|
||||||
def _draw_sidebar(self, rect: rl.Rectangle):
|
def _draw_sidebar(self, rect: rl.Rectangle):
|
||||||
rl.draw_rectangle_rec(rect, SIDEBAR_COLOR)
|
rl.draw_rectangle_rec(rect, SIDEBAR_COLOR)
|
||||||
|
|
||||||
@@ -109,17 +103,9 @@ class SettingsLayout:
|
|||||||
self._close_btn_rect = close_btn_rect
|
self._close_btn_rect = close_btn_rect
|
||||||
|
|
||||||
# Navigation buttons
|
# Navigation buttons
|
||||||
nav_start_y = rect.y + 300
|
y = rect.y + 300
|
||||||
button_spacing = 20
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
for panel_type, panel_info in self._panels.items():
|
for panel_type, panel_info in self._panels.items():
|
||||||
button_rect = rl.Rectangle(
|
button_rect = rl.Rectangle(rect.x + 50, y, rect.width - 150, NAV_BTN_HEIGHT)
|
||||||
rect.x + 50,
|
|
||||||
nav_start_y + i * (NAV_BTN_HEIGHT + button_spacing),
|
|
||||||
rect.width - 150, # Right-aligned with margin
|
|
||||||
NAV_BTN_HEIGHT,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Button styling
|
# Button styling
|
||||||
is_selected = panel_type == self._current_panel
|
is_selected = panel_type == self._current_panel
|
||||||
@@ -133,7 +119,8 @@ class SettingsLayout:
|
|||||||
|
|
||||||
# Store button rect for click detection
|
# Store button rect for click detection
|
||||||
panel_info.button_rect = button_rect
|
panel_info.button_rect = button_rect
|
||||||
i += 1
|
|
||||||
|
y += NAV_BTN_HEIGHT
|
||||||
|
|
||||||
def _draw_current_panel(self, rect: rl.Rectangle):
|
def _draw_current_panel(self, rect: rl.Rectangle):
|
||||||
rl.draw_rectangle_rounded(
|
rl.draw_rectangle_rounded(
|
||||||
@@ -144,17 +131,8 @@ class SettingsLayout:
|
|||||||
panel = self._panels[self._current_panel]
|
panel = self._panels[self._current_panel]
|
||||||
if panel.instance:
|
if panel.instance:
|
||||||
panel.instance.render(content_rect)
|
panel.instance.render(content_rect)
|
||||||
else:
|
|
||||||
gui_text_box(
|
|
||||||
content_rect,
|
|
||||||
f"Demo {self._panels[self._current_panel].name} Panel",
|
|
||||||
font_size=170,
|
|
||||||
color=rl.WHITE,
|
|
||||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
|
||||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle_mouse_release(self, mouse_pos: rl.Vector2) -> bool:
|
def _handle_mouse_release(self, mouse_pos: rl.Vector2) -> bool:
|
||||||
# Check close button
|
# Check close button
|
||||||
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
|
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
|
||||||
if self._close_callback:
|
if self._close_callback:
|
||||||
@@ -164,20 +142,15 @@ class SettingsLayout:
|
|||||||
# Check navigation buttons
|
# Check navigation buttons
|
||||||
for panel_type, panel_info in self._panels.items():
|
for panel_type, panel_info in self._panels.items():
|
||||||
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect):
|
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect):
|
||||||
self._switch_to_panel(panel_type)
|
self.set_current_panel(panel_type)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _switch_to_panel(self, panel_type: PanelType):
|
def set_current_panel(self, panel_type: PanelType):
|
||||||
if panel_type != self._current_panel:
|
if panel_type != self._current_panel:
|
||||||
self._current_panel = panel_type
|
self._current_panel = panel_type
|
||||||
|
|
||||||
def set_current_panel(self, index: int, param: str = ""):
|
|
||||||
panel_types = list(self._panels.keys())
|
|
||||||
if 0 <= index < len(panel_types):
|
|
||||||
self._switch_to_panel(panel_types[index])
|
|
||||||
|
|
||||||
def close_settings(self):
|
def close_settings(self):
|
||||||
if self._close_callback:
|
if self._close_callback:
|
||||||
self._close_callback()
|
self._close_callback()
|
||||||
|
|||||||
@@ -1,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):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self._params = Params()
|
||||||
|
items = self._init_items()
|
||||||
|
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||||
|
|
||||||
|
def _init_items(self):
|
||||||
items = [
|
items = [
|
||||||
text_item("Current Version", ""),
|
text_item("Current Version", ""),
|
||||||
button_item("Download", "CHECK", callback=self._on_download_update),
|
button_item("Download", "CHECK", callback=self._on_download_update),
|
||||||
@@ -9,13 +22,21 @@ class SoftwareLayout:
|
|||||||
button_item("Target Branch", "SELECT", callback=self._on_select_branch),
|
button_item("Target Branch", "SELECT", callback=self._on_select_branch),
|
||||||
button_item("Uninstall", "UNINSTALL", callback=self._on_uninstall),
|
button_item("Uninstall", "UNINSTALL", callback=self._on_uninstall),
|
||||||
]
|
]
|
||||||
|
return items
|
||||||
|
|
||||||
self._list_widget = ListView(items)
|
def _render(self, rect):
|
||||||
|
self._scroller.render(rect)
|
||||||
def render(self, rect):
|
|
||||||
self._list_widget.render(rect)
|
|
||||||
|
|
||||||
def _on_download_update(self): pass
|
def _on_download_update(self): pass
|
||||||
def _on_install_update(self): pass
|
def _on_install_update(self): pass
|
||||||
def _on_select_branch(self): pass
|
def _on_select_branch(self): pass
|
||||||
def _on_uninstall(self): pass
|
|
||||||
|
def _on_uninstall(self):
|
||||||
|
def handle_uninstall_confirmation(result):
|
||||||
|
if result == DialogResult.CONFIRM:
|
||||||
|
self._params.put_bool("DoUninstall", True)
|
||||||
|
|
||||||
|
gui_app.set_modal_overlay(
|
||||||
|
lambda: confirm_dialog("Are you sure you want to uninstall?", "Uninstall"),
|
||||||
|
callback=handle_uninstall_confirmation,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,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
|
from openpilot.common.params import Params
|
||||||
|
|
||||||
# Description constants
|
# Description constants
|
||||||
@@ -8,9 +10,14 @@ DESCRIPTIONS = {
|
|||||||
"Your attention is required at all times to use this feature."
|
"Your attention is required at all times to use this feature."
|
||||||
),
|
),
|
||||||
"DisengageOnAccelerator": "When enabled, pressing the accelerator pedal will disengage openpilot.",
|
"DisengageOnAccelerator": "When enabled, pressing the accelerator pedal will disengage openpilot.",
|
||||||
|
"LongitudinalPersonality": (
|
||||||
|
"Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " +
|
||||||
|
"In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " +
|
||||||
|
"your steering wheel distance button."
|
||||||
|
),
|
||||||
"IsLdwEnabled": (
|
"IsLdwEnabled": (
|
||||||
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
|
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
|
||||||
"without a turn signal activated while driving over 31 mph (50 km/h)."
|
"without a turn signal activated while driving over 31 mph (50 km/h)."
|
||||||
),
|
),
|
||||||
"AlwaysOnDM": "Enable driver monitoring even when openpilot is not engaged.",
|
"AlwaysOnDM": "Enable driver monitoring even when openpilot is not engaged.",
|
||||||
'RecordFront': "Upload data from the driver facing camera and help improve the driver monitoring algorithm.",
|
'RecordFront': "Upload data from the driver facing camera and help improve the driver monitoring algorithm.",
|
||||||
@@ -18,8 +25,9 @@ DESCRIPTIONS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TogglesLayout:
|
class TogglesLayout(Widget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
self._params = Params()
|
self._params = Params()
|
||||||
items = [
|
items = [
|
||||||
toggle_item(
|
toggle_item(
|
||||||
@@ -39,6 +47,15 @@ class TogglesLayout:
|
|||||||
self._params.get_bool("DisengageOnAccelerator"),
|
self._params.get_bool("DisengageOnAccelerator"),
|
||||||
icon="disengage_on_accelerator.png",
|
icon="disengage_on_accelerator.png",
|
||||||
),
|
),
|
||||||
|
multiple_button_item(
|
||||||
|
"Driving Personality",
|
||||||
|
DESCRIPTIONS["LongitudinalPersonality"],
|
||||||
|
buttons=["Aggressive", "Standard", "Relaxed"],
|
||||||
|
button_width=255,
|
||||||
|
callback=self._set_longitudinal_personality,
|
||||||
|
selected_index=int(self._params.get("LongitudinalPersonality") or 0),
|
||||||
|
icon="speed_limit.png"
|
||||||
|
),
|
||||||
toggle_item(
|
toggle_item(
|
||||||
"Enable Lane Departure Warnings",
|
"Enable Lane Departure Warnings",
|
||||||
DESCRIPTIONS["IsLdwEnabled"],
|
DESCRIPTIONS["IsLdwEnabled"],
|
||||||
@@ -62,7 +79,10 @@ class TogglesLayout:
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
self._list_widget = ListView(items)
|
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||||
|
|
||||||
def render(self, rect):
|
def _render(self, rect):
|
||||||
self._list_widget.render(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.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
|
from openpilot.system.ui.lib.widget import Widget
|
||||||
|
|
||||||
SIDEBAR_WIDTH = 300
|
SIDEBAR_WIDTH = 300
|
||||||
METRIC_HEIGHT = 126
|
METRIC_HEIGHT = 126
|
||||||
@@ -18,6 +19,7 @@ HOME_BTN = rl.Rectangle(60, 860, 180, 180)
|
|||||||
ThermalStatus = log.DeviceState.ThermalStatus
|
ThermalStatus = log.DeviceState.ThermalStatus
|
||||||
NetworkType = log.DeviceState.NetworkType
|
NetworkType = log.DeviceState.NetworkType
|
||||||
|
|
||||||
|
|
||||||
# Color scheme
|
# Color scheme
|
||||||
class Colors:
|
class Colors:
|
||||||
SIDEBAR_BG = rl.Color(57, 57, 57, 255)
|
SIDEBAR_BG = rl.Color(57, 57, 57, 255)
|
||||||
@@ -35,6 +37,7 @@ class Colors:
|
|||||||
BUTTON_NORMAL = rl.Color(255, 255, 255, 255)
|
BUTTON_NORMAL = rl.Color(255, 255, 255, 255)
|
||||||
BUTTON_PRESSED = rl.Color(255, 255, 255, 166)
|
BUTTON_PRESSED = rl.Color(255, 255, 255, 166)
|
||||||
|
|
||||||
|
|
||||||
NETWORK_TYPES = {
|
NETWORK_TYPES = {
|
||||||
NetworkType.none: "Offline",
|
NetworkType.none: "Offline",
|
||||||
NetworkType.wifi: "WiFi",
|
NetworkType.wifi: "WiFi",
|
||||||
@@ -57,8 +60,10 @@ class MetricData:
|
|||||||
self.value = value
|
self.value = value
|
||||||
self.color = color
|
self.color = color
|
||||||
|
|
||||||
class Sidebar:
|
|
||||||
|
class Sidebar(Widget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
self._net_type = NETWORK_TYPES.get(NetworkType.none)
|
self._net_type = NETWORK_TYPES.get(NetworkType.none)
|
||||||
self._net_strength = 0
|
self._net_strength = 0
|
||||||
|
|
||||||
@@ -72,7 +77,7 @@ class Sidebar:
|
|||||||
self._font_regular = gui_app.font(FontWeight.NORMAL)
|
self._font_regular = gui_app.font(FontWeight.NORMAL)
|
||||||
self._font_bold = gui_app.font(FontWeight.SEMI_BOLD)
|
self._font_bold = gui_app.font(FontWeight.SEMI_BOLD)
|
||||||
|
|
||||||
# Callbacks
|
# Callbacks
|
||||||
self._on_settings_click: Callable | None = None
|
self._on_settings_click: Callable | None = None
|
||||||
self._on_flag_click: Callable | None = None
|
self._on_flag_click: Callable | None = None
|
||||||
|
|
||||||
@@ -80,9 +85,7 @@ class Sidebar:
|
|||||||
self._on_settings_click = on_settings
|
self._on_settings_click = on_settings
|
||||||
self._on_flag_click = on_flag
|
self._on_flag_click = on_flag
|
||||||
|
|
||||||
def render(self, rect: rl.Rectangle):
|
def _render(self, rect: rl.Rectangle):
|
||||||
self.update_state()
|
|
||||||
|
|
||||||
# Background
|
# Background
|
||||||
rl.draw_rectangle_rec(rect, Colors.SIDEBAR_BG)
|
rl.draw_rectangle_rec(rect, Colors.SIDEBAR_BG)
|
||||||
|
|
||||||
@@ -90,9 +93,7 @@ class Sidebar:
|
|||||||
self._draw_network_indicator(rect)
|
self._draw_network_indicator(rect)
|
||||||
self._draw_metrics(rect)
|
self._draw_metrics(rect)
|
||||||
|
|
||||||
self._handle_mouse_release()
|
def _update_state(self):
|
||||||
|
|
||||||
def update_state(self):
|
|
||||||
sm = ui_state.sm
|
sm = ui_state.sm
|
||||||
if not sm.updated['deviceState']:
|
if not sm.updated['deviceState']:
|
||||||
return
|
return
|
||||||
@@ -134,11 +135,7 @@ class Sidebar:
|
|||||||
else:
|
else:
|
||||||
self._panda_status.update("VEHICLE", "ONLINE", Colors.GOOD)
|
self._panda_status.update("VEHICLE", "ONLINE", Colors.GOOD)
|
||||||
|
|
||||||
def _handle_mouse_release(self):
|
def _handle_mouse_release(self, mouse_pos: rl.Vector2):
|
||||||
if not rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
|
||||||
return
|
|
||||||
|
|
||||||
mouse_pos = rl.get_mouse_position()
|
|
||||||
if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN):
|
if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN):
|
||||||
if self._on_settings_click:
|
if self._on_settings_click:
|
||||||
self._on_settings_click()
|
self._on_settings_click()
|
||||||
@@ -148,8 +145,7 @@ class Sidebar:
|
|||||||
|
|
||||||
def _draw_buttons(self, rect: rl.Rectangle):
|
def _draw_buttons(self, rect: rl.Rectangle):
|
||||||
mouse_pos = rl.get_mouse_position()
|
mouse_pos = rl.get_mouse_position()
|
||||||
mouse_down = 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 button
|
||||||
settings_down = mouse_down and rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN)
|
settings_down = mouse_down and rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN)
|
||||||
|
|||||||
@@ -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.application import gui_app, FontWeight, DEFAULT_FPS
|
||||||
from openpilot.system.ui.lib.label import gui_text_box
|
from openpilot.system.ui.lib.label import gui_text_box
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
|
from openpilot.system.ui.lib.widget import Widget
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
|
|
||||||
|
|
||||||
ALERT_MARGIN = 40
|
ALERT_MARGIN = 40
|
||||||
ALERT_PADDING = 60
|
ALERT_PADDING = 60
|
||||||
ALERT_LINE_SPACING = 45
|
ALERT_LINE_SPACING = 45
|
||||||
@@ -21,7 +21,6 @@ ALERT_FONT_BIG = 88
|
|||||||
SELFDRIVE_STATE_TIMEOUT = 5 # Seconds
|
SELFDRIVE_STATE_TIMEOUT = 5 # Seconds
|
||||||
SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 # Seconds
|
SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 # Seconds
|
||||||
|
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
ALERT_COLORS = {
|
ALERT_COLORS = {
|
||||||
log.SelfdriveState.AlertStatus.normal: rl.Color(0, 0, 0, 235), # Black
|
log.SelfdriveState.AlertStatus.normal: rl.Color(0, 0, 0, 235), # Black
|
||||||
@@ -61,8 +60,9 @@ ALERT_CRITICAL_REBOOT = Alert(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AlertRenderer:
|
class AlertRenderer(Widget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
self.font_regular: rl.Font = gui_app.font(FontWeight.NORMAL)
|
self.font_regular: rl.Font = gui_app.font(FontWeight.NORMAL)
|
||||||
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||||
|
|
||||||
@@ -73,18 +73,20 @@ class AlertRenderer:
|
|||||||
# Check if selfdriveState messages have stopped arriving
|
# Check if selfdriveState messages have stopped arriving
|
||||||
if not sm.updated['selfdriveState']:
|
if not sm.updated['selfdriveState']:
|
||||||
recv_frame = sm.recv_frame['selfdriveState']
|
recv_frame = sm.recv_frame['selfdriveState']
|
||||||
if (sm.frame - recv_frame) > 5 * DEFAULT_FPS:
|
time_since_onroad = (sm.frame - ui_state.started_frame) / DEFAULT_FPS
|
||||||
# Check if waiting to start
|
|
||||||
if recv_frame < ui_state.started_frame:
|
|
||||||
return ALERT_STARTUP_PENDING
|
|
||||||
|
|
||||||
# Handle selfdrive timeout
|
# 1. Never received selfdriveState since going onroad
|
||||||
if TICI:
|
waiting_for_startup = recv_frame < ui_state.started_frame
|
||||||
ss_missing = time.monotonic() - sm.recv_time['selfdriveState']
|
if waiting_for_startup and time_since_onroad > 5:
|
||||||
if ss_missing > SELFDRIVE_STATE_TIMEOUT:
|
return ALERT_STARTUP_PENDING
|
||||||
if ss.enabled and (ss_missing - SELFDRIVE_STATE_TIMEOUT) < SELFDRIVE_UNRESPONSIVE_TIMEOUT:
|
|
||||||
return ALERT_CRITICAL_TIMEOUT
|
# 2. Lost communication with selfdriveState after receiving it
|
||||||
return ALERT_CRITICAL_REBOOT
|
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
|
# No alert if size is none
|
||||||
if ss.alertSize == 0:
|
if ss.alertSize == 0:
|
||||||
@@ -93,10 +95,10 @@ class AlertRenderer:
|
|||||||
# Return current alert
|
# Return current alert
|
||||||
return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize, status=ss.alertStatus)
|
return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize, status=ss.alertStatus)
|
||||||
|
|
||||||
def draw(self, rect: rl.Rectangle, sm: messaging.SubMaster) -> None:
|
def _render(self, rect: rl.Rectangle) -> bool:
|
||||||
alert = self.get_alert(sm)
|
alert = self.get_alert(ui_state.sm)
|
||||||
if not alert:
|
if not alert:
|
||||||
return
|
return False
|
||||||
|
|
||||||
alert_rect = self._get_alert_rect(rect, alert.size)
|
alert_rect = self._get_alert_rect(rect, alert.size)
|
||||||
self._draw_background(alert_rect, alert)
|
self._draw_background(alert_rect, alert)
|
||||||
@@ -108,13 +110,14 @@ class AlertRenderer:
|
|||||||
alert_rect.height - 2 * ALERT_PADDING
|
alert_rect.height - 2 * ALERT_PADDING
|
||||||
)
|
)
|
||||||
self._draw_text(text_rect, alert)
|
self._draw_text(text_rect, alert)
|
||||||
|
return True
|
||||||
|
|
||||||
def _get_alert_rect(self, rect: rl.Rectangle, size: int) -> rl.Rectangle:
|
def _get_alert_rect(self, rect: rl.Rectangle, size: int) -> rl.Rectangle:
|
||||||
if size == log.SelfdriveState.AlertSize.full:
|
if size == log.SelfdriveState.AlertSize.full:
|
||||||
return rect
|
return rect
|
||||||
|
|
||||||
height = (ALERT_FONT_MEDIUM + 2 * ALERT_PADDING if size == log.SelfdriveState.AlertSize.small else
|
height = (ALERT_FONT_MEDIUM + 2 * ALERT_PADDING if size == log.SelfdriveState.AlertSize.small else
|
||||||
ALERT_FONT_BIG + ALERT_LINE_SPACING + ALERT_FONT_SMALL + 2 * ALERT_PADDING)
|
ALERT_FONT_BIG + ALERT_LINE_SPACING + ALERT_FONT_SMALL + 2 * ALERT_PADDING)
|
||||||
|
|
||||||
return rl.Rectangle(
|
return rl.Rectangle(
|
||||||
rect.x + ALERT_MARGIN,
|
rect.x + ALERT_MARGIN,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
from collections.abc import Callable
|
||||||
from cereal import log
|
from cereal import log
|
||||||
from msgq.visionipc import VisionStreamType
|
from msgq.visionipc import VisionStreamType
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, UI_BORDER_SIZE
|
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, UI_BORDER_SIZE
|
||||||
@@ -13,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.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame
|
||||||
from openpilot.common.transformations.orientation import rot_from_euler
|
from openpilot.common.transformations.orientation import rot_from_euler
|
||||||
|
|
||||||
|
|
||||||
OpState = log.SelfdriveState.OpenpilotState
|
OpState = log.SelfdriveState.OpenpilotState
|
||||||
CALIBRATED = log.LiveCalibrationData.Status.calibrated
|
CALIBRATED = log.LiveCalibrationData.Status.calibrated
|
||||||
ROAD_CAM = VisionStreamType.VISION_STREAM_ROAD
|
ROAD_CAM = VisionStreamType.VISION_STREAM_ROAD
|
||||||
@@ -26,6 +25,9 @@ BORDER_COLORS = {
|
|||||||
UIStatus.ENGAGED: rl.Color(0x17, 0x86, 0x44, 0xF1), # Green for engaged state
|
UIStatus.ENGAGED: rl.Color(0x17, 0x86, 0x44, 0xF1), # Green for engaged state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WIDE_CAM_MAX_SPEED = 10.0 # m/s (22 mph)
|
||||||
|
ROAD_CAM_MIN_SPEED = 15.0 # m/s (34 mph)
|
||||||
|
|
||||||
|
|
||||||
class AugmentedRoadView(CameraView):
|
class AugmentedRoadView(CameraView):
|
||||||
def __init__(self, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_ROAD):
|
def __init__(self, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_ROAD):
|
||||||
@@ -47,11 +49,19 @@ class AugmentedRoadView(CameraView):
|
|||||||
self.alert_renderer = AlertRenderer()
|
self.alert_renderer = AlertRenderer()
|
||||||
self.driver_state_renderer = DriverStateRenderer()
|
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
|
# Only render when system is started to avoid invalid data access
|
||||||
if not ui_state.started:
|
if not ui_state.started:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self._switch_stream_if_needed(ui_state.sm)
|
||||||
|
|
||||||
# Update calibration before rendering
|
# Update calibration before rendering
|
||||||
self._update_calibration()
|
self._update_calibration()
|
||||||
|
|
||||||
@@ -76,13 +86,13 @@ class AugmentedRoadView(CameraView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Render the base camera view
|
# Render the base camera view
|
||||||
super().render(rect)
|
super()._render(rect)
|
||||||
|
|
||||||
# Draw all UI overlays
|
# Draw all UI overlays
|
||||||
self.model_renderer.draw(self._content_rect, ui_state.sm)
|
self.model_renderer.render(self._content_rect)
|
||||||
self._hud_renderer.draw(self._content_rect, ui_state.sm)
|
self._hud_renderer.render(self._content_rect)
|
||||||
self.alert_renderer.draw(self._content_rect, ui_state.sm)
|
if not self.alert_renderer.render(self._content_rect):
|
||||||
self.driver_state_renderer.draw(self._content_rect, ui_state.sm)
|
self.driver_state_renderer.render(self._content_rect)
|
||||||
|
|
||||||
# Custom UI extension point - add custom overlays here
|
# Custom UI extension point - add custom overlays here
|
||||||
# Use self._content_rect for positioning within camera bounds
|
# Use self._content_rect for positioning within camera bounds
|
||||||
@@ -90,10 +100,32 @@ class AugmentedRoadView(CameraView):
|
|||||||
# End clipping region
|
# End clipping region
|
||||||
rl.end_scissor_mode()
|
rl.end_scissor_mode()
|
||||||
|
|
||||||
|
# Handle click events if no HUD interaction occurred
|
||||||
|
if not self._hud_renderer.handle_mouse_event():
|
||||||
|
if self._click_callback and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||||
|
if rl.check_collision_point_rec(rl.get_mouse_position(), self._content_rect):
|
||||||
|
self._click_callback()
|
||||||
|
|
||||||
def _draw_border(self, rect: rl.Rectangle):
|
def _draw_border(self, rect: rl.Rectangle):
|
||||||
border_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED])
|
border_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED])
|
||||||
rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, border_color)
|
rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, border_color)
|
||||||
|
|
||||||
|
def _switch_stream_if_needed(self, sm):
|
||||||
|
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
|
||||||
|
v_ego = sm['carState'].vEgo
|
||||||
|
if v_ego < WIDE_CAM_MAX_SPEED:
|
||||||
|
target = WIDE_CAM
|
||||||
|
elif v_ego > ROAD_CAM_MIN_SPEED:
|
||||||
|
target = ROAD_CAM
|
||||||
|
else:
|
||||||
|
# Hysteresis zone - keep current stream
|
||||||
|
target = self.stream_type
|
||||||
|
else:
|
||||||
|
target = ROAD_CAM
|
||||||
|
|
||||||
|
if self.stream_type != target:
|
||||||
|
self.switch_stream(target)
|
||||||
|
|
||||||
def _update_calibration(self):
|
def _update_calibration(self):
|
||||||
# Update device camera if not already set
|
# Update device camera if not already set
|
||||||
sm = ui_state.sm
|
sm = ui_state.sm
|
||||||
@@ -129,7 +161,7 @@ class AugmentedRoadView(CameraView):
|
|||||||
|
|
||||||
# Get camera configuration
|
# Get camera configuration
|
||||||
device_camera = self.device_camera or DEFAULT_DEVICE_CAMERA
|
device_camera = self.device_camera or DEFAULT_DEVICE_CAMERA
|
||||||
is_wide_camera = self.stream_type == 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
|
intrinsic = device_camera.ecam.intrinsics if is_wide_camera else device_camera.fcam.intrinsics
|
||||||
calibration = self.view_from_wide_calib if is_wide_camera else self.view_from_calib
|
calibration = self.view_from_wide_calib if is_wide_camera else self.view_from_calib
|
||||||
zoom = 2.0 if is_wide_camera else 1.1
|
zoom = 2.0 if is_wide_camera else 1.1
|
||||||
@@ -170,9 +202,9 @@ class AugmentedRoadView(CameraView):
|
|||||||
])
|
])
|
||||||
|
|
||||||
video_transform = np.array([
|
video_transform = np.array([
|
||||||
[zoom, 0.0, (w / 2 + x - x_offset) - (cx * zoom)],
|
[zoom, 0.0, (w / 2 + x - x_offset) - (cx * zoom)],
|
||||||
[0.0, zoom, (h / 2 + y - y_offset) - (cy * zoom)],
|
[0.0, zoom, (h / 2 + y - y_offset) - (cy * zoom)],
|
||||||
[0.0, 0.0, 1.0]
|
[0.0, 0.0, 1.0]
|
||||||
])
|
])
|
||||||
self.model_renderer.set_transform(video_transform @ calib_transform)
|
self.model_renderer.set_transform(video_transform @ calib_transform)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import platform
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
|
||||||
@@ -6,12 +7,21 @@ from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
|
|||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
from openpilot.system.ui.lib.application import gui_app
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage
|
from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage
|
||||||
|
from openpilot.system.ui.lib.widget import Widget
|
||||||
|
|
||||||
CONNECTION_RETRY_INTERVAL = 0.2 # seconds between connection attempts
|
CONNECTION_RETRY_INTERVAL = 0.2 # seconds between connection attempts
|
||||||
|
|
||||||
VERTEX_SHADER = """
|
VERSION = """
|
||||||
#version 300 es
|
#version 300 es
|
||||||
|
precision mediump float;
|
||||||
|
"""
|
||||||
|
if platform.system() == "Darwin":
|
||||||
|
VERSION = """
|
||||||
|
#version 330 core
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
VERTEX_SHADER = VERSION + """
|
||||||
in vec3 vertexPosition;
|
in vec3 vertexPosition;
|
||||||
in vec2 vertexTexCoord;
|
in vec2 vertexTexCoord;
|
||||||
in vec3 vertexNormal;
|
in vec3 vertexNormal;
|
||||||
@@ -41,9 +51,7 @@ if TICI:
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
FRAME_FRAGMENT_SHADER = """
|
FRAME_FRAGMENT_SHADER = VERSION + """
|
||||||
#version 300 es
|
|
||||||
precision mediump float;
|
|
||||||
in vec2 fragTexCoord;
|
in vec2 fragTexCoord;
|
||||||
uniform sampler2D texture0;
|
uniform sampler2D texture0;
|
||||||
uniform sampler2D texture1;
|
uniform sampler2D texture1;
|
||||||
@@ -55,8 +63,10 @@ else:
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class CameraView:
|
|
||||||
|
class CameraView(Widget):
|
||||||
def __init__(self, name: str, stream_type: VisionStreamType):
|
def __init__(self, name: str, stream_type: VisionStreamType):
|
||||||
|
super().__init__()
|
||||||
self._name = name
|
self._name = name
|
||||||
# Primary stream
|
# Primary stream
|
||||||
self.client = VisionIpcClient(name, stream_type, conflate=True)
|
self.client = VisionIpcClient(name, stream_type, conflate=True)
|
||||||
@@ -68,7 +78,6 @@ class CameraView:
|
|||||||
self._target_stream_type: VisionStreamType | None = None
|
self._target_stream_type: VisionStreamType | None = None
|
||||||
self._switching: bool = False
|
self._switching: bool = False
|
||||||
|
|
||||||
|
|
||||||
self._texture_needs_update = True
|
self._texture_needs_update = True
|
||||||
self.last_connection_attempt: float = 0.0
|
self.last_connection_attempt: float = 0.0
|
||||||
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAME_FRAGMENT_SHADER)
|
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAME_FRAGMENT_SHADER)
|
||||||
@@ -82,7 +91,7 @@ class CameraView:
|
|||||||
self.egl_images: dict[int, EGLImage] = {}
|
self.egl_images: dict[int, EGLImage] = {}
|
||||||
self.egl_texture: rl.Texture | None = None
|
self.egl_texture: rl.Texture | None = None
|
||||||
|
|
||||||
self._placeholder_color : rl.Color | None = None
|
self._placeholder_color: rl.Color | None = None
|
||||||
|
|
||||||
# Initialize EGL for zero-copy rendering on TICI
|
# Initialize EGL for zero-copy rendering on TICI
|
||||||
if TICI:
|
if TICI:
|
||||||
@@ -145,12 +154,12 @@ class CameraView:
|
|||||||
zy = min(widget_aspect_ratio / frame_aspect_ratio, 1.0)
|
zy = min(widget_aspect_ratio / frame_aspect_ratio, 1.0)
|
||||||
|
|
||||||
return np.array([
|
return np.array([
|
||||||
[zx, 0.0, 0.0],
|
[zx, 0.0, 0.0],
|
||||||
[0.0, zy, 0.0],
|
[0.0, zy, 0.0],
|
||||||
[0.0, 0.0, 1.0]
|
[0.0, 0.0, 1.0]
|
||||||
])
|
])
|
||||||
|
|
||||||
def render(self, rect: rl.Rectangle):
|
def _render(self, rect: rl.Rectangle):
|
||||||
if self._switching:
|
if self._switching:
|
||||||
self._handle_switch()
|
self._handle_switch()
|
||||||
|
|
||||||
@@ -230,7 +239,7 @@ class CameraView:
|
|||||||
# Update textures with new frame data
|
# Update textures with new frame data
|
||||||
if self._texture_needs_update:
|
if self._texture_needs_update:
|
||||||
y_data = self.frame.data[: self.frame.uv_offset]
|
y_data = self.frame.data[: self.frame.uv_offset]
|
||||||
uv_data = self.frame.data[self.frame.uv_offset :]
|
uv_data = self.frame.data[self.frame.uv_offset:]
|
||||||
|
|
||||||
rl.update_texture(self.texture_y, rl.ffi.cast("void *", y_data.ctypes.data))
|
rl.update_texture(self.texture_y, rl.ffi.cast("void *", y_data.ctypes.data))
|
||||||
rl.update_texture(self.texture_uv, rl.ffi.cast("void *", uv_data.ctypes.data))
|
rl.update_texture(self.texture_uv, rl.ffi.cast("void *", uv_data.ctypes.data))
|
||||||
@@ -265,7 +274,7 @@ class CameraView:
|
|||||||
def _handle_switch(self) -> None:
|
def _handle_switch(self) -> None:
|
||||||
"""Check if target stream is ready and switch immediately."""
|
"""Check if target stream is ready and switch immediately."""
|
||||||
if not self._target_client or not self._switching:
|
if not self._target_client or not self._switching:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Try to connect target if needed
|
# Try to connect target if needed
|
||||||
if not self._target_client.is_connected():
|
if not self._target_client.is_connected():
|
||||||
@@ -277,28 +286,28 @@ class CameraView:
|
|||||||
# Check if target has frames ready
|
# Check if target has frames ready
|
||||||
target_frame = self._target_client.recv(timeout_ms=0)
|
target_frame = self._target_client.recv(timeout_ms=0)
|
||||||
if target_frame:
|
if target_frame:
|
||||||
self.frame = target_frame # Update current frame to target frame
|
self.frame = target_frame # Update current frame to target frame
|
||||||
self._complete_switch()
|
self._complete_switch()
|
||||||
|
|
||||||
def _complete_switch(self) -> None:
|
def _complete_switch(self) -> None:
|
||||||
"""Instantly switch to target stream."""
|
"""Instantly switch to target stream."""
|
||||||
cloudlog.debug(f"Switching to {self._target_stream_type}")
|
cloudlog.debug(f"Switching to {self._target_stream_type}")
|
||||||
# Clean up current resources
|
# Clean up current resources
|
||||||
if self.client:
|
if self.client:
|
||||||
del self.client
|
del self.client
|
||||||
|
|
||||||
# Switch to target
|
# Switch to target
|
||||||
self.client = self._target_client
|
self.client = self._target_client
|
||||||
self._stream_type = self._target_stream_type
|
self._stream_type = self._target_stream_type
|
||||||
self._texture_needs_update = True
|
self._texture_needs_update = True
|
||||||
|
|
||||||
# Reset state
|
# Reset state
|
||||||
self._target_client = None
|
self._target_client = None
|
||||||
self._target_stream_type = None
|
self._target_stream_type = None
|
||||||
self._switching = False
|
self._switching = False
|
||||||
|
|
||||||
# Initialize textures for new stream
|
# Initialize textures for new stream
|
||||||
self._initialize_textures()
|
self._initialize_textures()
|
||||||
|
|
||||||
def _initialize_textures(self):
|
def _initialize_textures(self):
|
||||||
self._clear_textures()
|
self._clear_textures()
|
||||||
|
|||||||
+19
-15
@@ -1,20 +1,23 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
from cereal import messaging
|
|
||||||
from msgq.visionipc import VisionStreamType
|
from msgq.visionipc import VisionStreamType
|
||||||
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
|
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
|
||||||
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
||||||
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.label import gui_label
|
from openpilot.system.ui.lib.label import gui_label
|
||||||
|
|
||||||
|
|
||||||
class DriverCameraView(CameraView):
|
class DriverCameraDialog(CameraView):
|
||||||
def __init__(self, stream_type: VisionStreamType):
|
def __init__(self):
|
||||||
super().__init__("camerad", stream_type)
|
super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER)
|
||||||
self.driver_state_renderer = DriverStateRenderer()
|
self.driver_state_renderer = DriverStateRenderer()
|
||||||
|
|
||||||
def render(self, rect, sm):
|
def _render(self, rect):
|
||||||
super().render(rect)
|
super()._render(rect)
|
||||||
|
|
||||||
|
if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||||
|
return 1
|
||||||
|
|
||||||
if not self.frame:
|
if not self.frame:
|
||||||
gui_label(
|
gui_label(
|
||||||
@@ -24,13 +27,15 @@ class DriverCameraView(CameraView):
|
|||||||
font_weight=FontWeight.BOLD,
|
font_weight=FontWeight.BOLD,
|
||||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
)
|
)
|
||||||
return
|
return -1
|
||||||
|
|
||||||
self._draw_face_detection(rect, sm)
|
self._draw_face_detection(rect)
|
||||||
self.driver_state_renderer.draw(rect, sm)
|
self.driver_state_renderer.render(rect)
|
||||||
|
|
||||||
def _draw_face_detection(self, rect: rl.Rectangle, sm) -> None:
|
return -1
|
||||||
driver_state = sm["driverStateV2"]
|
|
||||||
|
def _draw_face_detection(self, rect: rl.Rectangle) -> None:
|
||||||
|
driver_state = ui_state.sm["driverStateV2"]
|
||||||
is_rhd = driver_state.wheelOnRightProb > 0.5
|
is_rhd = driver_state.wheelOnRightProb > 0.5
|
||||||
driver_data = driver_state.rightDriverData if is_rhd else driver_state.leftDriverData
|
driver_data = driver_state.rightDriverData if is_rhd else driver_state.leftDriverData
|
||||||
face_detect = driver_data.faceProb > 0.7
|
face_detect = driver_data.faceProb > 0.7
|
||||||
@@ -83,12 +88,11 @@ class DriverCameraView(CameraView):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
gui_app.init_window("Driver Camera View")
|
gui_app.init_window("Driver Camera View")
|
||||||
sm = messaging.SubMaster(["selfdriveState", "driverStateV2", "driverMonitoringState"])
|
|
||||||
|
|
||||||
driver_camera_view = DriverCameraView(VisionStreamType.VISION_STREAM_DRIVER)
|
driver_camera_view = DriverCameraDialog()
|
||||||
try:
|
try:
|
||||||
for _ in gui_app.render():
|
for _ in gui_app.render():
|
||||||
sm.update()
|
ui_state.update()
|
||||||
driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height), sm)
|
driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||||
finally:
|
finally:
|
||||||
driver_camera_view.close()
|
driver_camera_view.close()
|
||||||
@@ -3,19 +3,19 @@ import pyray as rl
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, UI_BORDER_SIZE
|
from openpilot.selfdrive.ui.ui_state import ui_state, UI_BORDER_SIZE
|
||||||
from openpilot.system.ui.lib.application import gui_app
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
|
from openpilot.system.ui.lib.widget import Widget
|
||||||
|
|
||||||
# Default 3D coordinates for face keypoints as a NumPy array
|
# Default 3D coordinates for face keypoints as a NumPy array
|
||||||
DEFAULT_FACE_KPTS_3D = np.array([
|
DEFAULT_FACE_KPTS_3D = np.array([
|
||||||
[-5.98, -51.20, 8.00], [-17.64, -49.14, 8.00], [-23.81, -46.40, 8.00], [-29.98, -40.91, 8.00],
|
[-5.98, -51.20, 8.00], [-17.64, -49.14, 8.00], [-23.81, -46.40, 8.00], [-29.98, -40.91, 8.00],
|
||||||
[-32.04, -37.49, 8.00], [-34.10, -32.00, 8.00], [-36.16, -21.03, 8.00], [-36.16, 6.40, 8.00],
|
[-32.04, -37.49, 8.00], [-34.10, -32.00, 8.00], [-36.16, -21.03, 8.00], [-36.16, 6.40, 8.00],
|
||||||
[-35.47, 10.51, 8.00], [-32.73, 19.43, 8.00], [-29.30, 26.29, 8.00], [-24.50, 33.83, 8.00],
|
[-35.47, 10.51, 8.00], [-32.73, 19.43, 8.00], [-29.30, 26.29, 8.00], [-24.50, 33.83, 8.00],
|
||||||
[-19.01, 41.37, 8.00], [-14.21, 46.17, 8.00], [-12.16, 47.54, 8.00], [-4.61, 49.60, 8.00],
|
[-19.01, 41.37, 8.00], [-14.21, 46.17, 8.00], [-12.16, 47.54, 8.00], [-4.61, 49.60, 8.00],
|
||||||
[4.99, 49.60, 8.00], [12.53, 47.54, 8.00], [14.59, 46.17, 8.00], [19.39, 41.37, 8.00],
|
[4.99, 49.60, 8.00], [12.53, 47.54, 8.00], [14.59, 46.17, 8.00], [19.39, 41.37, 8.00],
|
||||||
[24.87, 33.83, 8.00], [29.67, 26.29, 8.00], [33.10, 19.43, 8.00], [35.84, 10.51, 8.00],
|
[24.87, 33.83, 8.00], [29.67, 26.29, 8.00], [33.10, 19.43, 8.00], [35.84, 10.51, 8.00],
|
||||||
[36.53, 6.40, 8.00], [36.53, -21.03, 8.00], [34.47, -32.00, 8.00], [32.42, -37.49, 8.00],
|
[36.53, 6.40, 8.00], [36.53, -21.03, 8.00], [34.47, -32.00, 8.00], [32.42, -37.49, 8.00],
|
||||||
[30.36, -40.91, 8.00], [24.19, -46.40, 8.00], [18.02, -49.14, 8.00], [6.36, -51.20, 8.00],
|
[30.36, -40.91, 8.00], [24.19, -46.40, 8.00], [18.02, -49.14, 8.00], [6.36, -51.20, 8.00],
|
||||||
[-5.98, -51.20, 8.00],
|
[-5.98, -51.20, 8.00],
|
||||||
], dtype=np.float32)
|
], dtype=np.float32)
|
||||||
|
|
||||||
# UI constants
|
# UI constants
|
||||||
@@ -31,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_POINT_COUNT = 37 # Number of points in the arc
|
||||||
ARC_ANGLES = np.linspace(0.0, np.pi, ARC_POINT_COUNT, dtype=np.float32)
|
ARC_ANGLES = np.linspace(0.0, np.pi, ARC_POINT_COUNT, dtype=np.float32)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ArcData:
|
class ArcData:
|
||||||
"""Data structure for arc rendering parameters."""
|
"""Data structure for arc rendering parameters."""
|
||||||
@@ -40,14 +41,15 @@ class ArcData:
|
|||||||
height: float
|
height: float
|
||||||
thickness: float
|
thickness: float
|
||||||
|
|
||||||
class DriverStateRenderer:
|
|
||||||
|
class DriverStateRenderer(Widget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
# Initial state with NumPy arrays
|
# Initial state with NumPy arrays
|
||||||
self.face_kpts_draw = DEFAULT_FACE_KPTS_3D.copy()
|
self.face_kpts_draw = DEFAULT_FACE_KPTS_3D.copy()
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
self.is_rhd = False
|
self.is_rhd = False
|
||||||
self.dm_fade_state = 0.0
|
self.dm_fade_state = 0.0
|
||||||
self.state_updated = False
|
|
||||||
self.last_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
|
self.last_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
|
||||||
self.driver_pose_vals = np.zeros(3, dtype=np.float32)
|
self.driver_pose_vals = np.zeros(3, dtype=np.float32)
|
||||||
self.driver_pose_diff = np.zeros(3, dtype=np.float32)
|
self.driver_pose_diff = np.zeros(3, dtype=np.float32)
|
||||||
@@ -73,14 +75,10 @@ class DriverStateRenderer:
|
|||||||
self.engaged_color = rl.Color(26, 242, 66, 255)
|
self.engaged_color = rl.Color(26, 242, 66, 255)
|
||||||
self.disengaged_color = rl.Color(139, 139, 139, 255)
|
self.disengaged_color = rl.Color(139, 139, 139, 255)
|
||||||
|
|
||||||
def draw(self, rect, sm):
|
self.set_visible(lambda: (ui_state.sm.recv_frame['driverStateV2'] > ui_state.started_frame and
|
||||||
if not self._is_visible(sm):
|
ui_state.sm.seen['driverMonitoringState']))
|
||||||
return
|
|
||||||
|
|
||||||
self._update_state(sm, rect)
|
|
||||||
if not self.state_updated:
|
|
||||||
return
|
|
||||||
|
|
||||||
|
def _render(self, rect):
|
||||||
# Set opacity based on active state
|
# Set opacity based on active state
|
||||||
opacity = 0.65 if self.is_active else 0.2
|
opacity = 0.65 if self.is_active else 0.2
|
||||||
|
|
||||||
@@ -105,18 +103,14 @@ class DriverStateRenderer:
|
|||||||
if self.v_arc_data:
|
if self.v_arc_data:
|
||||||
rl.draw_spline_linear(self.v_arc_lines, len(self.v_arc_lines), self.v_arc_data.thickness, self.arc_color)
|
rl.draw_spline_linear(self.v_arc_lines, len(self.v_arc_lines), self.v_arc_data.thickness, self.arc_color)
|
||||||
|
|
||||||
def _is_visible(self, sm):
|
def _update_state(self):
|
||||||
"""Check if the visualization should be rendered."""
|
|
||||||
return (sm.recv_frame['driverStateV2'] > ui_state.started_frame and
|
|
||||||
sm.seen['driverMonitoringState'] and
|
|
||||||
sm['selfdriveState'].alertSize == 0)
|
|
||||||
|
|
||||||
def _update_state(self, sm, rect):
|
|
||||||
"""Update the driver monitoring state based on model data"""
|
"""Update the driver monitoring state based on model data"""
|
||||||
if not sm.updated["driverMonitoringState"]:
|
sm = ui_state.sm
|
||||||
if self.state_updated and (rect.x != self.last_rect.x or rect.y != self.last_rect.y or \
|
if not sm.updated["driverMonitoringState"]:
|
||||||
rect.width != self.last_rect.width or rect.height != self.last_rect.height):
|
if (self._rect.x != self.last_rect.x or self._rect.y != self.last_rect.y or
|
||||||
self._pre_calculate_drawing_elements(rect)
|
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
|
return
|
||||||
|
|
||||||
# Get monitoring state
|
# Get monitoring state
|
||||||
@@ -165,16 +159,15 @@ class DriverStateRenderer:
|
|||||||
self.face_keypoints_transformed = self.face_kpts_draw[:, :2] * kp_depth[:, None]
|
self.face_keypoints_transformed = self.face_kpts_draw[:, :2] * kp_depth[:, None]
|
||||||
|
|
||||||
# Pre-calculate all drawing elements
|
# Pre-calculate all drawing elements
|
||||||
self._pre_calculate_drawing_elements(rect)
|
self._pre_calculate_drawing_elements()
|
||||||
self.state_updated = True
|
|
||||||
|
|
||||||
def _pre_calculate_drawing_elements(self, rect):
|
def _pre_calculate_drawing_elements(self):
|
||||||
"""Pre-calculate all drawing elements based on the current rectangle"""
|
"""Pre-calculate all drawing elements based on the current rectangle"""
|
||||||
# Calculate icon position (bottom-left or bottom-right)
|
# Calculate icon position (bottom-left or bottom-right)
|
||||||
width, height = rect.width, rect.height
|
width, height = self._rect.width, self._rect.height
|
||||||
offset = UI_BORDER_SIZE + BTN_SIZE // 2
|
offset = UI_BORDER_SIZE + BTN_SIZE // 2
|
||||||
self.position_x = rect.x + (width - offset if self.is_rhd else offset)
|
self.position_x = self._rect.x + (width - offset if self.is_rhd else offset)
|
||||||
self.position_y = rect.y + height - offset
|
self.position_y = self._rect.y + height - offset
|
||||||
|
|
||||||
# Pre-calculate the face lines positions
|
# Pre-calculate the face lines positions
|
||||||
positioned_keypoints = self.face_keypoints_transformed + np.array([self.position_x, self.position_y])
|
positioned_keypoints = self.face_keypoints_transformed + np.array([self.position_x, self.position_y])
|
||||||
@@ -189,15 +182,15 @@ class DriverStateRenderer:
|
|||||||
# Horizontal arc
|
# Horizontal arc
|
||||||
h_width = abs(delta_x)
|
h_width = abs(delta_x)
|
||||||
self.h_arc_data = self._calculate_arc_data(
|
self.h_arc_data = self._calculate_arc_data(
|
||||||
delta_x, h_width, self.position_x, self.position_y - ARC_LENGTH / 2,
|
delta_x, h_width, self.position_x, self.position_y - ARC_LENGTH / 2,
|
||||||
self.driver_pose_sins[1], self.driver_pose_diff[1], is_horizontal=True
|
self.driver_pose_sins[1], self.driver_pose_diff[1], is_horizontal=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Vertical arc
|
# Vertical arc
|
||||||
v_height = abs(delta_y)
|
v_height = abs(delta_y)
|
||||||
self.v_arc_data = self._calculate_arc_data(
|
self.v_arc_data = self._calculate_arc_data(
|
||||||
delta_y, v_height, self.position_x - ARC_LENGTH / 2, self.position_y,
|
delta_y, v_height, self.position_x - ARC_LENGTH / 2, self.position_y,
|
||||||
self.driver_pose_sins[0], self.driver_pose_diff[0], is_horizontal=False
|
self.driver_pose_sins[0], self.driver_pose_diff[0], is_horizontal=False
|
||||||
)
|
)
|
||||||
|
|
||||||
def _calculate_arc_data(
|
def _calculate_arc_data(
|
||||||
|
|||||||
@@ -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
|
import pyray as rl
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from cereal.messaging import SubMaster
|
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||||
|
from openpilot.selfdrive.ui.onroad.exp_button import ExpButton
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.common.conversions import Conversions as CV
|
from openpilot.common.conversions import Conversions as CV
|
||||||
|
from openpilot.system.ui.lib.widget import Widget
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
SET_SPEED_NA = 255
|
SET_SPEED_NA = 255
|
||||||
@@ -54,21 +55,25 @@ FONT_SIZES = FontSizes()
|
|||||||
COLORS = Colors()
|
COLORS = Colors()
|
||||||
|
|
||||||
|
|
||||||
class HudRenderer:
|
class HudRenderer(Widget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
"""Initialize the HUD renderer."""
|
"""Initialize the HUD renderer."""
|
||||||
self.is_cruise_set: bool = False
|
self.is_cruise_set: bool = False
|
||||||
self.is_cruise_available: bool = False
|
self.is_cruise_available: bool = False
|
||||||
self.set_speed: float = SET_SPEED_NA
|
self.set_speed: float = SET_SPEED_NA
|
||||||
self.speed: float = 0.0
|
self.speed: float = 0.0
|
||||||
self.v_ego_cluster_seen: bool = False
|
self.v_ego_cluster_seen: bool = False
|
||||||
self._wheel_texture: rl.Texture = gui_app.texture('icons/chffr_wheel.png', UI_CONFIG.wheel_icon_size, UI_CONFIG.wheel_icon_size)
|
|
||||||
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
|
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
|
||||||
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||||
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
|
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
|
||||||
|
|
||||||
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."""
|
"""Update HUD state based on car state and controls state."""
|
||||||
|
sm = ui_state.sm
|
||||||
if sm.recv_frame["carState"] < ui_state.started_frame:
|
if sm.recv_frame["carState"] < ui_state.started_frame:
|
||||||
self.is_cruise_set = False
|
self.is_cruise_set = False
|
||||||
self.set_speed = SET_SPEED_NA
|
self.set_speed = SET_SPEED_NA
|
||||||
@@ -94,9 +99,9 @@ class HudRenderer:
|
|||||||
speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||||
self.speed = max(0.0, v_ego * speed_conversion)
|
self.speed = max(0.0, v_ego * speed_conversion)
|
||||||
|
|
||||||
def draw(self, rect: rl.Rectangle, sm: SubMaster) -> None:
|
def _render(self, rect: rl.Rectangle) -> None:
|
||||||
"""Render HUD elements to the screen."""
|
"""Render HUD elements to the screen."""
|
||||||
self._update_state(sm)
|
# Draw the header background
|
||||||
rl.draw_rectangle_gradient_v(
|
rl.draw_rectangle_gradient_v(
|
||||||
int(rect.x),
|
int(rect.x),
|
||||||
int(rect.y),
|
int(rect.y),
|
||||||
@@ -110,7 +115,13 @@ class HudRenderer:
|
|||||||
self._draw_set_speed(rect)
|
self._draw_set_speed(rect)
|
||||||
|
|
||||||
self._draw_current_speed(rect)
|
self._draw_current_speed(rect)
|
||||||
self._draw_wheel_icon(rect)
|
|
||||||
|
button_x = rect.x + rect.width - UI_CONFIG.border_size - UI_CONFIG.button_size
|
||||||
|
button_y = rect.y + UI_CONFIG.border_size
|
||||||
|
self._exp_button.render(rl.Rectangle(button_x, button_y, UI_CONFIG.button_size, UI_CONFIG.button_size))
|
||||||
|
|
||||||
|
def handle_mouse_event(self) -> bool:
|
||||||
|
return bool(self._exp_button.handle_mouse_event())
|
||||||
|
|
||||||
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
|
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
|
||||||
"""Draw the MAX speed indicator box."""
|
"""Draw the MAX speed indicator box."""
|
||||||
@@ -166,13 +177,3 @@ class HudRenderer:
|
|||||||
unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit)
|
unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit)
|
||||||
unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2)
|
unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2)
|
||||||
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.white_translucent)
|
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.white_translucent)
|
||||||
|
|
||||||
def _draw_wheel_icon(self, rect: rl.Rectangle) -> None:
|
|
||||||
"""Draw the steering wheel icon with status-based opacity."""
|
|
||||||
center_x = int(rect.x + rect.width - UI_CONFIG.border_size - UI_CONFIG.button_size / 2)
|
|
||||||
center_y = int(rect.y + UI_CONFIG.border_size + UI_CONFIG.button_size / 2)
|
|
||||||
rl.draw_circle(center_x, center_y, UI_CONFIG.button_size / 2, COLORS.black_translucent)
|
|
||||||
|
|
||||||
opacity = 0.7 if ui_state.status == UIStatus.DISENGAGED else 1.0
|
|
||||||
img_pos = rl.Vector2(center_x - self._wheel_texture.width / 2, center_y - self._wheel_texture.height / 2)
|
|
||||||
rl.draw_texture_v(self._wheel_texture, img_pos, rl.Color(255, 255, 255, int(255 * opacity)))
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ from openpilot.common.params import Params
|
|||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.system.ui.lib.application import DEFAULT_FPS
|
from openpilot.system.ui.lib.application import DEFAULT_FPS
|
||||||
from openpilot.system.ui.lib.shader_polygon import draw_polygon
|
from openpilot.system.ui.lib.shader_polygon import draw_polygon
|
||||||
|
from openpilot.system.ui.lib.widget import Widget
|
||||||
from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT
|
from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT
|
||||||
|
|
||||||
|
|
||||||
CLIP_MARGIN = 500
|
CLIP_MARGIN = 500
|
||||||
MIN_DRAW_DISTANCE = 10.0
|
MIN_DRAW_DISTANCE = 10.0
|
||||||
MAX_DRAW_DISTANCE = 100.0
|
MAX_DRAW_DISTANCE = 100.0
|
||||||
@@ -36,6 +36,7 @@ class ModelPoints:
|
|||||||
raw_points: np.ndarray = field(default_factory=lambda: np.empty((0, 3), dtype=np.float32))
|
raw_points: np.ndarray = field(default_factory=lambda: np.empty((0, 3), dtype=np.float32))
|
||||||
projected_points: np.ndarray = field(default_factory=lambda: np.empty((0, 2), dtype=np.float32))
|
projected_points: np.ndarray = field(default_factory=lambda: np.empty((0, 2), dtype=np.float32))
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LeadVehicle:
|
class LeadVehicle:
|
||||||
glow: list[float] = field(default_factory=list)
|
glow: list[float] = field(default_factory=list)
|
||||||
@@ -43,8 +44,9 @@ class LeadVehicle:
|
|||||||
fill_alpha: int = 0
|
fill_alpha: int = 0
|
||||||
|
|
||||||
|
|
||||||
class ModelRenderer:
|
class ModelRenderer(Widget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
self._longitudinal_control = False
|
self._longitudinal_control = False
|
||||||
self._experimental_mode = False
|
self._experimental_mode = False
|
||||||
self._blend_factor = 1.0
|
self._blend_factor = 1.0
|
||||||
@@ -64,11 +66,6 @@ class ModelRenderer:
|
|||||||
self._car_space_transform = np.zeros((3, 3), dtype=np.float32)
|
self._car_space_transform = np.zeros((3, 3), dtype=np.float32)
|
||||||
self._transform_dirty = True
|
self._transform_dirty = True
|
||||||
self._clip_region = None
|
self._clip_region = None
|
||||||
self._rect = None
|
|
||||||
|
|
||||||
# Pre-allocated arrays for polygon conversion
|
|
||||||
self._temp_points_3d = np.empty((MAX_POINTS * 2, 3), dtype=np.float32)
|
|
||||||
self._temp_proj = np.empty((3, MAX_POINTS * 2), dtype=np.float32)
|
|
||||||
|
|
||||||
self._exp_gradient = {
|
self._exp_gradient = {
|
||||||
'start': (0.0, 1.0), # Bottom of path
|
'start': (0.0, 1.0), # Bottom of path
|
||||||
@@ -86,14 +83,15 @@ class ModelRenderer:
|
|||||||
self._car_space_transform = transform.astype(np.float32)
|
self._car_space_transform = transform.astype(np.float32)
|
||||||
self._transform_dirty = True
|
self._transform_dirty = True
|
||||||
|
|
||||||
def 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
|
# Check if data is up-to-date
|
||||||
if (sm.recv_frame["liveCalibration"] < ui_state.started_frame or
|
if (sm.recv_frame["liveCalibration"] < ui_state.started_frame or
|
||||||
sm.recv_frame["modelV2"] < ui_state.started_frame):
|
sm.recv_frame["modelV2"] < ui_state.started_frame):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Set up clipping region
|
# Set up clipping region
|
||||||
self._rect = rect
|
|
||||||
self._clip_region = rl.Rectangle(
|
self._clip_region = rl.Rectangle(
|
||||||
rect.x - CLIP_MARGIN, rect.y - CLIP_MARGIN, rect.width + 2 * CLIP_MARGIN, rect.height + 2 * CLIP_MARGIN
|
rect.x - CLIP_MARGIN, rect.y - CLIP_MARGIN, rect.width + 2 * CLIP_MARGIN, rect.height + 2 * CLIP_MARGIN
|
||||||
)
|
)
|
||||||
@@ -127,7 +125,6 @@ class ModelRenderer:
|
|||||||
self._update_leads(radar_state, path_x_array)
|
self._update_leads(radar_state, path_x_array)
|
||||||
self._transform_dirty = False
|
self._transform_dirty = False
|
||||||
|
|
||||||
|
|
||||||
# Draw elements
|
# Draw elements
|
||||||
self._draw_lane_lines()
|
self._draw_lane_lines()
|
||||||
self._draw_path(sm)
|
self._draw_path(sm)
|
||||||
@@ -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)]
|
glow = [(x + (sz * 1.35) + g_xo, y + sz + g_yo), (x, y - g_yo), (x - (sz * 1.35) - g_xo, y + sz + g_yo)]
|
||||||
chevron = [(x + (sz * 1.25), y + sz), (x, y), (x - (sz * 1.25), y + sz)]
|
chevron = [(x + (sz * 1.25), y + sz), (x, y), (x - (sz * 1.25), y + sz)]
|
||||||
|
|
||||||
return LeadVehicle(glow=glow,chevron=chevron, fill_alpha=int(fill_alpha))
|
return LeadVehicle(glow=glow, chevron=chevron, fill_alpha=int(fill_alpha))
|
||||||
|
|
||||||
def _draw_lane_lines(self):
|
def _draw_lane_lines(self):
|
||||||
"""Draw lane lines and road edges"""
|
"""Draw lane lines and road edges"""
|
||||||
@@ -357,54 +354,60 @@ class ModelRenderer:
|
|||||||
if points.shape[0] == 0:
|
if points.shape[0] == 0:
|
||||||
return np.empty((0, 2), dtype=np.float32)
|
return np.empty((0, 2), dtype=np.float32)
|
||||||
|
|
||||||
# Create left and right 3D points in one array
|
N = points.shape[0]
|
||||||
n_points = points.shape[0]
|
# Generate left and right 3D points in one array using broadcasting
|
||||||
points_3d = self._temp_points_3d[:n_points * 2]
|
offsets = np.array([[0, -y_off, z_off], [0, y_off, z_off]], dtype=np.float32)
|
||||||
points_3d[:n_points, 0] = points_3d[n_points:, 0] = points[:, 0]
|
points_3d = points[None, :, :] + offsets[:, None, :] # Shape: 2xNx3
|
||||||
points_3d[:n_points, 1] = points[:, 1] - y_off
|
points_3d = points_3d.reshape(2 * N, 3) # Shape: (2*N)x3
|
||||||
points_3d[n_points:, 1] = points[:, 1] + y_off
|
|
||||||
points_3d[:n_points, 2] = points_3d[n_points:, 2] = points[:, 2] + z_off
|
|
||||||
|
|
||||||
# Single matrix multiplication for projections
|
# Transform all points to projected space in one operation
|
||||||
proj = np.ascontiguousarray(self._temp_proj[:, :n_points * 2]) # Slice the pre-allocated array
|
proj = self._car_space_transform @ points_3d.T # Shape: 3x(2*N)
|
||||||
np.dot(self._car_space_transform, points_3d.T, out=proj)
|
proj = proj.reshape(3, 2, N)
|
||||||
valid_z = np.abs(proj[2]) > 1e-6
|
left_proj = proj[:, 0, :]
|
||||||
if not np.any(valid_z):
|
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)
|
return np.empty((0, 2), dtype=np.float32)
|
||||||
|
|
||||||
# Compute screen coordinates
|
# Compute screen coordinates
|
||||||
screen = proj[:2, valid_z] / proj[2, valid_z][None, :]
|
left_screen = left_proj[:2, valid_proj] / left_proj[2, valid_proj][None, :]
|
||||||
left_screen = screen[:, :n_points].T
|
right_screen = right_proj[:2, valid_proj] / right_proj[2, valid_proj][None, :]
|
||||||
right_screen = screen[:, n_points:].T
|
|
||||||
|
|
||||||
# Ensure consistent shapes by re-aligning valid points
|
# Define clip region bounds
|
||||||
valid_points = np.minimum(left_screen.shape[0], right_screen.shape[0])
|
clip = self._clip_region
|
||||||
if valid_points == 0:
|
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)
|
return np.empty((0, 2), dtype=np.float32)
|
||||||
left_screen = left_screen[:valid_points]
|
|
||||||
right_screen = right_screen[:valid_points]
|
|
||||||
|
|
||||||
if self._clip_region:
|
# Select valid and clipped points
|
||||||
clip = self._clip_region
|
left_screen = left_screen[:, both_in_clip]
|
||||||
bounds_mask = (
|
right_screen = right_screen[:, both_in_clip]
|
||||||
(left_screen[:, 0] >= clip.x) & (left_screen[:, 0] <= clip.x + clip.width) &
|
|
||||||
(left_screen[:, 1] >= clip.y) & (left_screen[:, 1] <= clip.y + clip.height) &
|
# Handle Y-coordinate inversion on hills
|
||||||
(right_screen[:, 0] >= clip.x) & (right_screen[:, 0] <= clip.x + clip.width) &
|
if not allow_invert and left_screen.shape[1] > 1:
|
||||||
(right_screen[:, 1] >= clip.y) & (right_screen[:, 1] <= clip.y + clip.height)
|
y = left_screen[1, :] # y-coordinates
|
||||||
)
|
keep = y == np.minimum.accumulate(y)
|
||||||
if not np.any(bounds_mask):
|
if not np.any(keep):
|
||||||
return np.empty((0, 2), dtype=np.float32)
|
return np.empty((0, 2), dtype=np.float32)
|
||||||
left_screen = left_screen[bounds_mask]
|
left_screen = left_screen[:, keep]
|
||||||
right_screen = right_screen[bounds_mask]
|
right_screen = right_screen[:, keep]
|
||||||
|
|
||||||
if not allow_invert and left_screen.shape[0] > 1:
|
return np.vstack((left_screen.T, right_screen[:, ::-1].T)).astype(np.float32)
|
||||||
keep = np.concatenate(([True], np.diff(left_screen[:, 1]) < 0))
|
|
||||||
left_screen = left_screen[keep]
|
|
||||||
right_screen = right_screen[keep]
|
|
||||||
if left_screen.shape[0] == 0:
|
|
||||||
return np.empty((0, 2), dtype=np.float32)
|
|
||||||
|
|
||||||
return np.vstack((left_screen, right_screen[::-1])).astype(np.float32)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _map_val(x, x0, x1, y0, y1):
|
def _map_val(x, x0, x1, y0, y1):
|
||||||
@@ -417,10 +420,10 @@ class ModelRenderer:
|
|||||||
def _hsla_to_color(h, s, l, a):
|
def _hsla_to_color(h, s, l, a):
|
||||||
rgb = colorsys.hls_to_rgb(h, l, s)
|
rgb = colorsys.hls_to_rgb(h, l, s)
|
||||||
return rl.Color(
|
return rl.Color(
|
||||||
int(rgb[0] * 255),
|
int(rgb[0] * 255),
|
||||||
int(rgb[1] * 255),
|
int(rgb[1] * 255),
|
||||||
int(rgb[2] * 255),
|
int(rgb[2] * 255),
|
||||||
int(a * 255)
|
int(a * 255)
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ FirehosePanel::FirehosePanel(SettingsWindow *parent) : QWidget((QWidget*)parent)
|
|||||||
layout->setSpacing(20);
|
layout->setSpacing(20);
|
||||||
|
|
||||||
// header
|
// header
|
||||||
QLabel *title = new QLabel(tr("🔥 Firehose Mode 🔥"));
|
QLabel *title = new QLabel(tr("Firehose Mode"));
|
||||||
title->setStyleSheet("font-size: 100px; font-weight: 500; font-family: 'Noto Color Emoji';");
|
title->setStyleSheet("font-size: 100px; font-weight: 500; font-family: 'Noto Color Emoji';");
|
||||||
layout->addWidget(title, 0, Qt::AlignCenter);
|
layout->addWidget(title, 0, Qt::AlignCenter);
|
||||||
|
|
||||||
|
|||||||
@@ -139,6 +139,15 @@ void TogglesPanel::expandToggleDescription(const QString ¶m) {
|
|||||||
toggles[param.toStdString()]->showDescription();
|
toggles[param.toStdString()]->showDescription();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TogglesPanel::scrollToToggle(const QString ¶m) {
|
||||||
|
if (auto it = toggles.find(param.toStdString()); it != toggles.end()) {
|
||||||
|
auto scroll_area = qobject_cast<QScrollArea*>(parent()->parent());
|
||||||
|
if (scroll_area) {
|
||||||
|
scroll_area->ensureWidgetVisible(it->second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void TogglesPanel::showEvent(QShowEvent *event) {
|
void TogglesPanel::showEvent(QShowEvent *event) {
|
||||||
updateToggles();
|
updateToggles();
|
||||||
}
|
}
|
||||||
@@ -222,7 +231,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
|
|||||||
addItem(dcamBtn);
|
addItem(dcamBtn);
|
||||||
#endif
|
#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::showDescriptionEvent, this, &DevicePanel::updateCalibDescription);
|
||||||
connect(resetCalibBtn, &ButtonControl::clicked, [&]() {
|
connect(resetCalibBtn, &ButtonControl::clicked, [&]() {
|
||||||
if (!uiState()->engaged()) {
|
if (!uiState()->engaged()) {
|
||||||
@@ -235,6 +244,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
|
|||||||
params.remove("LiveParametersV2");
|
params.remove("LiveParametersV2");
|
||||||
params.remove("LiveDelay");
|
params.remove("LiveDelay");
|
||||||
params.putBool("OnroadCycleRequested", true);
|
params.putBool("OnroadCycleRequested", true);
|
||||||
|
updateCalibDescription();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -313,9 +323,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DevicePanel::updateCalibDescription() {
|
void DevicePanel::updateCalibDescription() {
|
||||||
QString desc =
|
QString desc = tr("sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down.");
|
||||||
tr("sunnypilot requires the device to be mounted within 4° left or right and "
|
|
||||||
"within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required.");
|
|
||||||
std::string calib_bytes = params.get("CalibrationParams");
|
std::string calib_bytes = params.get("CalibrationParams");
|
||||||
if (!calib_bytes.empty()) {
|
if (!calib_bytes.empty()) {
|
||||||
try {
|
try {
|
||||||
@@ -333,8 +341,53 @@ void DevicePanel::updateCalibDescription() {
|
|||||||
qInfo() << "invalid CalibrationParams";
|
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() {
|
void DevicePanel::reboot() {
|
||||||
@@ -387,6 +440,7 @@ void SettingsWindow::setCurrentPanel(int index, const QString ¶m) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
emit expandToggleDescription(param);
|
emit expandToggleDescription(param);
|
||||||
|
emit scrollToToggle(param);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,6 +481,7 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) {
|
|||||||
|
|
||||||
TogglesPanel *toggles = new TogglesPanel(this);
|
TogglesPanel *toggles = new TogglesPanel(this);
|
||||||
QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription);
|
QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription);
|
||||||
|
QObject::connect(this, &SettingsWindow::scrollToToggle, toggles, &TogglesPanel::scrollToToggle);
|
||||||
|
|
||||||
auto networking = new Networking(this);
|
auto networking = new Networking(this);
|
||||||
QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType);
|
QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType);
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ signals:
|
|||||||
void reviewTrainingGuide();
|
void reviewTrainingGuide();
|
||||||
void showDriverView();
|
void showDriverView();
|
||||||
void expandToggleDescription(const QString ¶m);
|
void expandToggleDescription(const QString ¶m);
|
||||||
|
void scrollToToggle(const QString ¶m);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QPushButton *sidebar_alert_widget;
|
QPushButton *sidebar_alert_widget;
|
||||||
@@ -67,6 +68,7 @@ protected slots:
|
|||||||
protected:
|
protected:
|
||||||
Params params;
|
Params params;
|
||||||
ButtonControl *pair_device;
|
ButtonControl *pair_device;
|
||||||
|
ButtonControl *resetCalibBtn;
|
||||||
};
|
};
|
||||||
|
|
||||||
class TogglesPanel : public ListWidget {
|
class TogglesPanel : public ListWidget {
|
||||||
@@ -77,6 +79,7 @@ public:
|
|||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void expandToggleDescription(const QString ¶m);
|
void expandToggleDescription(const QString ¶m);
|
||||||
|
void scrollToToggle(const QString ¶m);
|
||||||
|
|
||||||
protected slots:
|
protected slots:
|
||||||
virtual void updateState(const UIState &s);
|
virtual void updateState(const UIState &s);
|
||||||
|
|||||||
@@ -136,6 +136,59 @@ QWidget * Setup::low_voltage() {
|
|||||||
return widget;
|
return widget;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QWidget * Setup::custom_software_warning() {
|
||||||
|
QWidget *widget = new QWidget();
|
||||||
|
QVBoxLayout *main_layout = new QVBoxLayout(widget);
|
||||||
|
main_layout->setContentsMargins(55, 0, 55, 55);
|
||||||
|
main_layout->setSpacing(0);
|
||||||
|
|
||||||
|
QVBoxLayout *inner_layout = new QVBoxLayout();
|
||||||
|
inner_layout->setContentsMargins(110, 110, 300, 0);
|
||||||
|
main_layout->addLayout(inner_layout);
|
||||||
|
|
||||||
|
QLabel *title = new QLabel(tr("WARNING: Custom Software"));
|
||||||
|
title->setStyleSheet("font-size: 90px; font-weight: 500; color: #FF594F;");
|
||||||
|
inner_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft);
|
||||||
|
|
||||||
|
inner_layout->addSpacing(25);
|
||||||
|
|
||||||
|
QLabel *body = new QLabel(tr("Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.\n\nIf you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later."));
|
||||||
|
body->setWordWrap(true);
|
||||||
|
body->setAlignment(Qt::AlignTop | Qt::AlignLeft);
|
||||||
|
body->setStyleSheet("font-size: 65px; font-weight: 300;");
|
||||||
|
inner_layout->addWidget(body);
|
||||||
|
|
||||||
|
inner_layout->addStretch();
|
||||||
|
|
||||||
|
QHBoxLayout *blayout = new QHBoxLayout();
|
||||||
|
blayout->setSpacing(50);
|
||||||
|
main_layout->addLayout(blayout, 0);
|
||||||
|
|
||||||
|
QPushButton *back = new QPushButton(tr("Back"));
|
||||||
|
back->setObjectName("navBtn");
|
||||||
|
blayout->addWidget(back);
|
||||||
|
QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage);
|
||||||
|
|
||||||
|
QPushButton *cont = new QPushButton(tr("Continue"));
|
||||||
|
cont->setObjectName("navBtn");
|
||||||
|
blayout->addWidget(cont);
|
||||||
|
QObject::connect(cont, &QPushButton::clicked, this, [=]() {
|
||||||
|
QTimer::singleShot(0, [=]() {
|
||||||
|
setCurrentWidget(downloading_widget);
|
||||||
|
});
|
||||||
|
QString url = InputDialog::getText(tr("Enter URL"), this, tr("for Custom Software"));
|
||||||
|
if (!url.isEmpty()) {
|
||||||
|
QTimer::singleShot(1000, this, [=]() {
|
||||||
|
download(url);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setCurrentWidget(software_selection_widget);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
|
||||||
QWidget * Setup::getting_started() {
|
QWidget * Setup::getting_started() {
|
||||||
QWidget *widget = new QWidget();
|
QWidget *widget = new QWidget();
|
||||||
|
|
||||||
@@ -305,20 +358,17 @@ QWidget * Setup::software_selection() {
|
|||||||
blayout->addWidget(cont);
|
blayout->addWidget(cont);
|
||||||
|
|
||||||
QObject::connect(cont, &QPushButton::clicked, [=]() {
|
QObject::connect(cont, &QPushButton::clicked, [=]() {
|
||||||
auto w = currentWidget();
|
|
||||||
QTimer::singleShot(0, [=]() {
|
|
||||||
setCurrentWidget(downloading_widget);
|
|
||||||
});
|
|
||||||
QString url = OPENPILOT_URL;
|
|
||||||
if (group->checkedButton() != openpilot) {
|
if (group->checkedButton() != openpilot) {
|
||||||
url = InputDialog::getText(tr("Enter URL"), this, tr("for Custom Software"));
|
QTimer::singleShot(0, [=]() {
|
||||||
}
|
setCurrentWidget(custom_software_warning_widget);
|
||||||
if (!url.isEmpty()) {
|
|
||||||
QTimer::singleShot(1000, this, [=]() {
|
|
||||||
download(url);
|
|
||||||
});
|
});
|
||||||
} else {
|
} 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(getting_started());
|
||||||
addWidget(network_setup());
|
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();
|
downloading_widget = downloading();
|
||||||
addWidget(downloading_widget);
|
addWidget(downloading_widget);
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
void selectLanguage();
|
void selectLanguage();
|
||||||
QWidget *low_voltage();
|
QWidget *low_voltage();
|
||||||
|
QWidget *custom_software_warning();
|
||||||
QWidget *getting_started();
|
QWidget *getting_started();
|
||||||
QWidget *network_setup();
|
QWidget *network_setup();
|
||||||
QWidget *software_selection();
|
QWidget *software_selection();
|
||||||
@@ -23,6 +24,8 @@ private:
|
|||||||
|
|
||||||
QWidget *failed_widget;
|
QWidget *failed_widget;
|
||||||
QWidget *downloading_widget;
|
QWidget *downloading_widget;
|
||||||
|
QWidget *custom_software_warning_widget;
|
||||||
|
QWidget *software_selection_widget;
|
||||||
QTranslator translator;
|
QTranslator translator;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
|||||||
@@ -87,6 +87,17 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) {
|
|||||||
params.put("DeviceBootMode", QString::number(index).toStdString());
|
params.put("DeviceBootMode", QString::number(index).toStdString());
|
||||||
updateState();
|
updateState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interactivityTimeout = new OptionControlSP("InteractivityTimeout", tr("Interactivity Timeout"),
|
||||||
|
tr("Apply a custom timeout for settings UI."
|
||||||
|
"\nThis is the time after which settings UI closes automatically if user is not interacting with the screen."),
|
||||||
|
"", {0, 120}, 10, true, nullptr, false);
|
||||||
|
|
||||||
|
connect(interactivityTimeout, &OptionControlSP::updateLabels, [=]() {
|
||||||
|
updateState();
|
||||||
|
});
|
||||||
|
|
||||||
|
addItem(interactivityTimeout);
|
||||||
|
|
||||||
// Brightness
|
// Brightness
|
||||||
brightness = new Brightness();
|
brightness = new Brightness();
|
||||||
@@ -198,4 +209,11 @@ void DevicePanelSP::updateState() {
|
|||||||
currStatus = DeviceSleepModeStatus::OFFROAD;
|
currStatus = DeviceSleepModeStatus::OFFROAD;
|
||||||
}
|
}
|
||||||
toggleDeviceBootMode->setDescription(deviceSleepModeDescription(currStatus));
|
toggleDeviceBootMode->setDescription(deviceSleepModeDescription(currStatus));
|
||||||
|
|
||||||
|
QString timeoutValue = QString::fromStdString(params.get("InteractivityTimeout"));
|
||||||
|
if (timeoutValue == "0") {
|
||||||
|
interactivityTimeout->setLabel("DEFAULT");
|
||||||
|
} else {
|
||||||
|
interactivityTimeout->setLabel(timeoutValue + "s");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ private:
|
|||||||
MaxTimeOffroad *maxTimeOffroad;
|
MaxTimeOffroad *maxTimeOffroad;
|
||||||
ButtonParamControlSP *toggleDeviceBootMode;
|
ButtonParamControlSP *toggleDeviceBootMode;
|
||||||
Brightness *brightness;
|
Brightness *brightness;
|
||||||
|
OptionControlSP *interactivityTimeout;
|
||||||
|
|
||||||
const QString alwaysOffroadStyle = R"(
|
const QString alwaysOffroadStyle = R"(
|
||||||
PushButtonSP {
|
PushButtonSP {
|
||||||
|
|||||||
@@ -7,21 +7,55 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
|
#include <QStyle>
|
||||||
|
|
||||||
#include "common/model.h"
|
#include "common/model.h"
|
||||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h"
|
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h"
|
||||||
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
|
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
|
||||||
|
|
||||||
|
static const QString progressStyleActive = "QProgressBar {"
|
||||||
|
" font-size: 40px;"
|
||||||
|
" font-weight: 200;"
|
||||||
|
" padding: 1px;"
|
||||||
|
" border: 3px solid black;"
|
||||||
|
" border-radius: 10px;"
|
||||||
|
"}"
|
||||||
|
"QProgressBar::chunk {"
|
||||||
|
" background-color: #1e79e8;"
|
||||||
|
" border-radius: 10px;"
|
||||||
|
"}";
|
||||||
|
|
||||||
|
static const QString progressStyleInactive = progressStyleActive +
|
||||||
|
"QProgressBar::chunk {"
|
||||||
|
" background-color: transparent;"
|
||||||
|
"}";
|
||||||
|
|
||||||
|
static const QString progressStyleDone = progressStyleActive +
|
||||||
|
"QProgressBar {"
|
||||||
|
" color: #33ab4c;"
|
||||||
|
"}"
|
||||||
|
"QProgressBar::chunk {"
|
||||||
|
" background-color: transparent;"
|
||||||
|
"}";
|
||||||
|
|
||||||
|
static const QString progressStyleError = progressStyleActive +
|
||||||
|
"QProgressBar {"
|
||||||
|
" color: red;"
|
||||||
|
"}"
|
||||||
|
"QProgressBar::chunk {"
|
||||||
|
" background-color: transparent;"
|
||||||
|
"}";
|
||||||
|
|
||||||
ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) {
|
ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) {
|
||||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||||
main_layout->setContentsMargins(50, 20, 50, 20);
|
main_layout->setContentsMargins(50, 20, 50, 20);
|
||||||
|
|
||||||
ListWidgetSP *list = new ListWidgetSP(this);
|
ListWidgetSP *list = new ListWidgetSP(this, false);
|
||||||
ScrollViewSP *scroller = new ScrollViewSP(list, this);
|
ScrollViewSP *scroller = new ScrollViewSP(list, this);
|
||||||
main_layout->addWidget(scroller);
|
main_layout->addWidget(scroller);
|
||||||
|
|
||||||
const auto current_model = GetActiveModelName();
|
const auto current_model = GetActiveModelName();
|
||||||
currentModelLblBtn = new ButtonControlSP(tr("Current Model"), tr("SELECT"), current_model);
|
currentModelLblBtn = new ButtonControlSP(tr("Current Model"), tr("SELECT"), "", this);
|
||||||
currentModelLblBtn->setValue(current_model);
|
currentModelLblBtn->setValue(current_model);
|
||||||
|
|
||||||
connect(currentModelLblBtn, &ButtonControlSP::clicked, this, &ModelsPanel::handleCurrentModelLblBtnClicked);
|
connect(currentModelLblBtn, &ButtonControlSP::clicked, this, &ModelsPanel::handleCurrentModelLblBtnClicked);
|
||||||
@@ -32,23 +66,83 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) {
|
|||||||
connect(uiStateSP(), &UIStateSP::uiUpdate, this, &ModelsPanel::updateLabels);
|
connect(uiStateSP(), &UIStateSP::uiUpdate, this, &ModelsPanel::updateLabels);
|
||||||
list->addItem(currentModelLblBtn);
|
list->addItem(currentModelLblBtn);
|
||||||
|
|
||||||
|
// Create progress bars for downloads
|
||||||
|
supercomboProgressBar = createProgressBar(this);
|
||||||
|
QString supercomboType = tr("Driving Model");
|
||||||
|
supercomboFrame = createModelDetailFrame(this, supercomboType, supercomboProgressBar);
|
||||||
|
list->addItem(supercomboFrame);
|
||||||
|
|
||||||
|
navigationProgressBar = createProgressBar(this);
|
||||||
|
QString navigationType = tr("Navigation Model");
|
||||||
|
navigationFrame = createModelDetailFrame(this, navigationType, navigationProgressBar);
|
||||||
|
list->addItem(navigationFrame);
|
||||||
|
|
||||||
|
visionProgressBar = createProgressBar(this);
|
||||||
|
QString visionType = tr("Vision Model");
|
||||||
|
visionFrame = createModelDetailFrame(this, visionType, visionProgressBar);
|
||||||
|
list->addItem(visionFrame);
|
||||||
|
|
||||||
|
policyProgressBar = createProgressBar(this);
|
||||||
|
QString policyType = tr("Policy Model");
|
||||||
|
policyFrame = createModelDetailFrame(this, policyType, policyProgressBar);
|
||||||
|
list->addItem(policyFrame);
|
||||||
|
|
||||||
|
list->addItem(horizontal_line());
|
||||||
|
|
||||||
// LiveDelay toggle
|
// LiveDelay toggle
|
||||||
list->addItem(new ParamControlSP("LagdToggle",
|
lagd_toggle_control = new ParamControlSP("LagdToggle", tr("Live Learning Steer Delay"), "", "../assets/offroad/icon_shell.png");
|
||||||
tr("Live Learning Steer Delay"),
|
lagd_toggle_control->showDescription();
|
||||||
tr("Enable this for the car to learn and adapt its steering response time. "
|
list->addItem(lagd_toggle_control);
|
||||||
"Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience."),
|
|
||||||
"../assets/offroad/icon_shell.png"));
|
// Software delay control
|
||||||
|
delay_control = new OptionControlSP("LagdToggledelay", tr("Adjust Software Delay"),
|
||||||
|
tr("Adjust the software delay when Live Learning Steer Delay is toggled off."
|
||||||
|
"\nThe default software delay value is 0.2"),
|
||||||
|
"", {10, 30}, 1, false, nullptr, true);
|
||||||
|
|
||||||
|
connect(delay_control, &OptionControlSP::updateLabels, [=]() {
|
||||||
|
float value = QString::fromStdString(params.get("LagdToggledelay")).toFloat();
|
||||||
|
delay_control->setLabel(QString::number(value, 'f', 2) + "s");
|
||||||
|
});
|
||||||
|
connect(lagd_toggle_control, &ParamControlSP::toggleFlipped, [=](bool state) {
|
||||||
|
delay_control->setVisible(!state);
|
||||||
|
});
|
||||||
|
delay_control->showDescription();
|
||||||
|
list->addItem(delay_control);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QProgressBar* ModelsPanel::createProgressBar(QWidget *parent) {
|
||||||
|
QProgressBar *progressBar = new QProgressBar(parent);
|
||||||
|
progressBar->setRange(0, 100);
|
||||||
|
progressBar->setValue(0);
|
||||||
|
progressBar->setTextVisible(true);
|
||||||
|
progressBar->setAlignment(Qt::AlignVCenter);
|
||||||
|
return progressBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFrame* ModelsPanel::createModelDetailFrame(QWidget *parent, QString &typeName, QProgressBar *progressBar) {
|
||||||
|
QFrame *frame = new QFrame(parent);
|
||||||
|
QHBoxLayout *layout = new QHBoxLayout(frame);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->setSpacing(50);
|
||||||
|
layout->addWidget(new QLabel(typeName));
|
||||||
|
layout->addWidget(progressBar);
|
||||||
|
frame->setVisible(false);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Updates the UI with bundle download progress information
|
* @brief Updates the UI with bundle download progress information
|
||||||
* Reads status from modelManagerSP cereal message and displays status for all models
|
* Reads status from modelManagerSP cereal message and displays status for all models
|
||||||
*/
|
*/
|
||||||
void ModelsPanel::handleBundleDownloadProgress() {
|
void ModelsPanel::handleBundleDownloadProgress() {
|
||||||
|
supercomboFrame->setVisible(false);
|
||||||
|
visionFrame->setVisible(false);
|
||||||
|
policyFrame->setVisible(false);
|
||||||
|
navigationFrame->setVisible(false);
|
||||||
|
|
||||||
using DS = cereal::ModelManagerSP::DownloadStatus;
|
using DS = cereal::ModelManagerSP::DownloadStatus;
|
||||||
if (!model_manager.hasSelectedBundle() && !model_manager.hasActiveBundle()) {
|
if (!model_manager.hasSelectedBundle() && !model_manager.hasActiveBundle()) {
|
||||||
currentModelLblBtn->setDescription(tr("No custom model selected!"));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,21 +155,27 @@ void ModelsPanel::handleBundleDownloadProgress() {
|
|||||||
|
|
||||||
// Get status for each model type in order
|
// Get status for each model type in order
|
||||||
for (const auto &model: models) {
|
for (const auto &model: models) {
|
||||||
QString typeName;
|
|
||||||
QString modelName = QString::fromStdString(bundle.getDisplayName());
|
QString modelName = QString::fromStdString(bundle.getDisplayName());
|
||||||
|
|
||||||
|
QProgressBar *progressBar = nullptr;
|
||||||
|
QFrame *modelFrame = nullptr;
|
||||||
|
|
||||||
switch (model.getType()) {
|
switch (model.getType()) {
|
||||||
case cereal::ModelManagerSP::Model::Type::SUPERCOMBO:
|
case cereal::ModelManagerSP::Model::Type::SUPERCOMBO:
|
||||||
typeName = tr("Driving");
|
progressBar = supercomboProgressBar;
|
||||||
|
modelFrame = supercomboFrame;
|
||||||
break;
|
break;
|
||||||
case cereal::ModelManagerSP::Model::Type::NAVIGATION:
|
case cereal::ModelManagerSP::Model::Type::NAVIGATION:
|
||||||
typeName = tr("Navigation");
|
progressBar = navigationProgressBar;
|
||||||
|
modelFrame = navigationFrame;
|
||||||
break;
|
break;
|
||||||
case cereal::ModelManagerSP::Model::Type::VISION:
|
case cereal::ModelManagerSP::Model::Type::VISION:
|
||||||
typeName = tr("Vision");
|
progressBar = visionProgressBar;
|
||||||
|
modelFrame = visionFrame;
|
||||||
break;
|
break;
|
||||||
case cereal::ModelManagerSP::Model::Type::POLICY:
|
case cereal::ModelManagerSP::Model::Type::POLICY:
|
||||||
typeName = tr("Policy");
|
progressBar = policyProgressBar;
|
||||||
|
modelFrame = policyFrame;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,31 +183,26 @@ void ModelsPanel::handleBundleDownloadProgress() {
|
|||||||
QString line;
|
QString line;
|
||||||
|
|
||||||
if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING) {
|
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) {
|
} 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) {
|
} 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) {
|
} 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 {
|
} else {
|
||||||
line = tr("%1 model [%2] pending...").arg(typeName, modelName);
|
progressBar->setStyleSheet(progressStyleInactive);
|
||||||
|
progressBar->setFormat(tr(" pending - %1").arg(modelName));
|
||||||
}
|
}
|
||||||
status.append(line);
|
// keep navigation hidden for now to avoid confusion
|
||||||
}
|
if (model.getType() != cereal::ModelManagerSP::Model::Type::NAVIGATION) {
|
||||||
|
modelFrame->setVisible(true);
|
||||||
currentModelLblBtn->setDescription(status.join("\n"));
|
|
||||||
|
|
||||||
if (prev_download_status != download_status) {
|
|
||||||
switch (bundle.getStatus()) {
|
|
||||||
case cereal::ModelManagerSP::DownloadStatus::DOWNLOADING:
|
|
||||||
case cereal::ModelManagerSP::DownloadStatus::CACHED:
|
|
||||||
case cereal::ModelManagerSP::DownloadStatus::DOWNLOADED:
|
|
||||||
currentModelLblBtn->showDescription();
|
|
||||||
break;
|
|
||||||
case cereal::ModelManagerSP::DownloadStatus::FAILED:
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prev_download_status = download_status;
|
prev_download_status = download_status;
|
||||||
@@ -125,6 +220,17 @@ QString ModelsPanel::GetActiveModelName() {
|
|||||||
return DEFAULT_MODEL;
|
return DEFAULT_MODEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Gets the short name of the currently selected model bundle
|
||||||
|
* @return Display short name of the selected bundle or default model name
|
||||||
|
*/
|
||||||
|
QString ModelsPanel::GetActiveModelInternalName() {
|
||||||
|
if (model_manager.hasActiveBundle()) {
|
||||||
|
return QString::fromStdString(model_manager.getActiveBundle().getInternalName());
|
||||||
|
}
|
||||||
|
return DEFAULT_MODEL;
|
||||||
|
}
|
||||||
|
|
||||||
void ModelsPanel::updateModelManagerState() {
|
void ModelsPanel::updateModelManagerState() {
|
||||||
const SubMaster &sm = *(uiStateSP()->sm);
|
const SubMaster &sm = *(uiStateSP()->sm);
|
||||||
model_manager = sm["modelManagerSP"].getModelManagerSP();
|
model_manager = sm["modelManagerSP"].getModelManagerSP();
|
||||||
@@ -156,7 +262,7 @@ void ModelsPanel::handleCurrentModelLblBtnClicked() {
|
|||||||
bundleNames.append(index_to_bundle[index]);
|
bundleNames.append(index_to_bundle[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentModelLblBtn->setValue(GetActiveModelName());
|
currentModelLblBtn->setValue(GetActiveModelInternalName());
|
||||||
|
|
||||||
const QString selectedBundleName = MultiOptionDialog::getSelection(
|
const QString selectedBundleName = MultiOptionDialog::getSelection(
|
||||||
tr("Select a Model"), bundleNames, GetActiveModelName(), this);
|
tr("Select a Model"), bundleNames, GetActiveModelName(), this);
|
||||||
@@ -197,7 +303,25 @@ void ModelsPanel::updateLabels() {
|
|||||||
updateModelManagerState();
|
updateModelManagerState();
|
||||||
handleBundleDownloadProgress();
|
handleBundleDownloadProgress();
|
||||||
currentModelLblBtn->setEnabled(!is_onroad && !isDownloading());
|
currentModelLblBtn->setEnabled(!is_onroad && !isDownloading());
|
||||||
currentModelLblBtn->setValue(GetActiveModelName());
|
currentModelLblBtn->setValue(GetActiveModelInternalName());
|
||||||
|
|
||||||
|
// Update lagdToggle description with current value
|
||||||
|
QString desc = tr("Enable this for the car to learn and adapt its steering response time. "
|
||||||
|
"Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. "
|
||||||
|
"The Current value is updated automatically when the vehicle is Onroad.");
|
||||||
|
QString current = QString::fromStdString(params.get("LagdToggleDesc", false));
|
||||||
|
if (!current.isEmpty()) {
|
||||||
|
desc += "<br><br><b><span style=\"color:#e0e0e0\">" + tr("Current:") + "</span></b> <span style=\"color:#e0e0e0\">" + current + "</span>";
|
||||||
|
}
|
||||||
|
lagd_toggle_control->setDescription(desc);
|
||||||
|
lagd_toggle_control->showDescription();
|
||||||
|
|
||||||
|
delay_control->setVisible(!params.getBool("LagdToggle"));
|
||||||
|
if (delay_control->isVisible()) {
|
||||||
|
float value = QString::fromStdString(params.get("LagdToggledelay")).toFloat();
|
||||||
|
delay_control->setLabel(QString::number(value, 'f', 2) + "s");
|
||||||
|
delay_control->showDescription();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <QProgressBar>
|
||||||
|
|
||||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||||
|
|
||||||
class ModelsPanel : public QWidget {
|
class ModelsPanel : public QWidget {
|
||||||
@@ -17,6 +19,7 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
QString GetActiveModelName();
|
QString GetActiveModelName();
|
||||||
|
QString GetActiveModelInternalName();
|
||||||
void updateModelManagerState();
|
void updateModelManagerState();
|
||||||
|
|
||||||
bool isDownloading() const {
|
bool isDownloading() const {
|
||||||
@@ -33,6 +36,8 @@ private:
|
|||||||
void handleCurrentModelLblBtnClicked();
|
void handleCurrentModelLblBtnClicked();
|
||||||
void handleBundleDownloadProgress();
|
void handleBundleDownloadProgress();
|
||||||
void showResetParamsDialog();
|
void showResetParamsDialog();
|
||||||
|
QProgressBar* createProgressBar(QWidget *parent);
|
||||||
|
QFrame* createModelDetailFrame(QWidget *parent, QString &typeName, QProgressBar *progressBar);
|
||||||
cereal::ModelManagerSP::Reader model_manager;
|
cereal::ModelManagerSP::Reader model_manager;
|
||||||
cereal::ModelManagerSP::DownloadStatus download_status{};
|
cereal::ModelManagerSP::DownloadStatus download_status{};
|
||||||
cereal::ModelManagerSP::DownloadStatus prev_download_status{};
|
cereal::ModelManagerSP::DownloadStatus prev_download_status{};
|
||||||
@@ -59,6 +64,16 @@ private:
|
|||||||
bool is_onroad = false;
|
bool is_onroad = false;
|
||||||
|
|
||||||
ButtonControlSP *currentModelLblBtn;
|
ButtonControlSP *currentModelLblBtn;
|
||||||
|
ParamControlSP *lagd_toggle_control;
|
||||||
|
OptionControlSP *delay_control;
|
||||||
|
QProgressBar *supercomboProgressBar;
|
||||||
|
QFrame *supercomboFrame;
|
||||||
|
QProgressBar *navigationProgressBar;
|
||||||
|
QFrame *navigationFrame;
|
||||||
|
QProgressBar *visionProgressBar;
|
||||||
|
QFrame *visionFrame;
|
||||||
|
QProgressBar *policyProgressBar;
|
||||||
|
QFrame *policyFrame;
|
||||||
Params params;
|
Params params;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) {
|
|||||||
|
|
||||||
TogglesPanelSP *toggles = new TogglesPanelSP(this);
|
TogglesPanelSP *toggles = new TogglesPanelSP(this);
|
||||||
QObject::connect(this, &SettingsWindowSP::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription);
|
QObject::connect(this, &SettingsWindowSP::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription);
|
||||||
|
QObject::connect(this, &SettingsWindowSP::scrollToToggle, toggles, &TogglesPanel::scrollToToggle);
|
||||||
|
|
||||||
auto networking = new NetworkingSP(this);
|
auto networking = new NetworkingSP(this);
|
||||||
QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &NetworkingSP::setPrimeType);
|
QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &NetworkingSP::setPrimeType);
|
||||||
|
|||||||
@@ -480,6 +480,16 @@ private:
|
|||||||
return result.toInt();
|
return result.toInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int getParamValueScaled() {
|
||||||
|
const auto param_value = QString::fromStdString(params.get(key));
|
||||||
|
return static_cast<int>(param_value.toFloat() * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setParamValueScaled(const int new_value) {
|
||||||
|
const float scaled_value = new_value / 100.0f;
|
||||||
|
params.put(key, QString::number(scaled_value, 'f', 2).toStdString());
|
||||||
|
}
|
||||||
|
|
||||||
// Although the method is not static, and thus has access to the value property, I prefer to be explicit about the value.
|
// Although the method is not static, and thus has access to the value property, I prefer to be explicit about the value.
|
||||||
void setParamValue(const int new_value) {
|
void setParamValue(const int new_value) {
|
||||||
const auto value_str = valueMap != nullptr ? valueMap->value(QString::number(new_value)) : QString::number(new_value);
|
const auto value_str = valueMap != nullptr ? valueMap->value(QString::number(new_value)) : QString::number(new_value);
|
||||||
@@ -488,7 +498,8 @@ private:
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
OptionControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon,
|
OptionControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon,
|
||||||
const MinMaxValue &range, const int per_value_change = 1, const bool inline_layout = false, const QMap<QString, QString> *valMap = nullptr) : AbstractControlSP_SELECTOR(title, desc, icon, nullptr), _title(title), valueMap(valMap), is_inline_layout(inline_layout) {
|
const MinMaxValue &range, const int per_value_change = 1, const bool inline_layout = false,
|
||||||
|
const QMap<QString, QString> *valMap = nullptr, bool scale_float = false) : AbstractControlSP_SELECTOR(title, desc, icon, nullptr), _title(title), valueMap(valMap), is_inline_layout(inline_layout), use_float_scaling(scale_float) {
|
||||||
const QString style = R"(
|
const QString style = R"(
|
||||||
QPushButton {
|
QPushButton {
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
@@ -528,7 +539,7 @@ public:
|
|||||||
const std::vector<QString> button_texts{"-", "+"};
|
const std::vector<QString> button_texts{"-", "+"};
|
||||||
|
|
||||||
key = param.toStdString();
|
key = param.toStdString();
|
||||||
value = getParamValue();
|
value = use_float_scaling ? getParamValueScaled() : getParamValue();
|
||||||
|
|
||||||
button_group = new QButtonGroup(this);
|
button_group = new QButtonGroup(this);
|
||||||
button_group->setExclusive(true);
|
button_group->setExclusive(true);
|
||||||
@@ -546,10 +557,15 @@ public:
|
|||||||
|
|
||||||
QObject::connect(button, &QPushButton::clicked, [=]() {
|
QObject::connect(button, &QPushButton::clicked, [=]() {
|
||||||
int change_value = (i == 0) ? -per_value_change : per_value_change;
|
int change_value = (i == 0) ? -per_value_change : per_value_change;
|
||||||
value = getParamValue(); // in case it changed externally, we need to get the latest value.
|
value = use_float_scaling ? getParamValueScaled() : getParamValue();
|
||||||
value += change_value;
|
value += change_value;
|
||||||
value = std::clamp(value, range.min_value, range.max_value);
|
value = std::clamp(value, range.min_value, range.max_value);
|
||||||
setParamValue(value);
|
|
||||||
|
if (use_float_scaling) {
|
||||||
|
setParamValueScaled(value);
|
||||||
|
} else {
|
||||||
|
setParamValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
button_group->button(0)->setEnabled(!(value <= range.min_value));
|
button_group->button(0)->setEnabled(!(value <= range.min_value));
|
||||||
button_group->button(1)->setEnabled(!(value >= range.max_value));
|
button_group->button(1)->setEnabled(!(value >= range.max_value));
|
||||||
@@ -642,6 +658,7 @@ private:
|
|||||||
const QString label_disabled_style = "font-size: 50px; font-weight: 450; color: #5C5C5C;";
|
const QString label_disabled_style = "font-size: 50px; font-weight: 450; color: #5C5C5C;";
|
||||||
|
|
||||||
bool button_enabled = true;
|
bool button_enabled = true;
|
||||||
|
bool use_float_scaling = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
class PushButtonSP : public QPushButton {
|
class PushButtonSP : public QPushButton {
|
||||||
|
|||||||
@@ -391,9 +391,9 @@ def create_screenshots():
|
|||||||
driver_img = frames[2]
|
driver_img = frames[2]
|
||||||
else:
|
else:
|
||||||
with open(frames_cache, 'wb') as f:
|
with open(frames_cache, 'wb') as f:
|
||||||
road_img = FrameReader(route.camera_paths()[segnum]).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]).get(0, pix_fmt="nv12")[0]
|
wide_road_img = FrameReader(route.ecamera_paths()[segnum], pix_fmt="nv12").get(0)
|
||||||
driver_img = FrameReader(route.dcamera_paths()[segnum]).get(0, pix_fmt="nv12")[0]
|
driver_img = FrameReader(route.dcamera_paths()[segnum], pix_fmt="nv12").get(0)
|
||||||
pickle.dump([road_img, wide_road_img, driver_img], f)
|
pickle.dump([road_img, wide_road_img, driver_img], f)
|
||||||
|
|
||||||
STREAMS.append((VisionStreamType.VISION_STREAM_ROAD, cam.fcam, road_img.flatten().tobytes()))
|
STREAMS.append((VisionStreamType.VISION_STREAM_ROAD, cam.fcam, road_img.flatten().tobytes()))
|
||||||
|
|||||||
+2
-1
@@ -164,8 +164,9 @@ void Device::setAwake(bool on) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Device::resetInteractiveTimeout(int timeout) {
|
void Device::resetInteractiveTimeout(int timeout) {
|
||||||
|
int customTimeout = QString::fromStdString(Params().get("InteractivityTimeout")).toInt();
|
||||||
if (timeout == -1) {
|
if (timeout == -1) {
|
||||||
timeout = (ignition_on ? 10 : 30);
|
timeout = customTimeout == 0 ? (ignition_on ? 10 : 30) : customTimeout;
|
||||||
}
|
}
|
||||||
interactive_timeout = timeout * UI_FREQ;
|
interactive_timeout = timeout * UI_FREQ;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -6,16 +6,16 @@ from openpilot.selfdrive.ui.layouts.main import MainLayout
|
|||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
gui_app.init_window("UI")
|
gui_app.init_window("UI")
|
||||||
main_layout = MainLayout()
|
main_layout = MainLayout()
|
||||||
|
main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||||
for _ in gui_app.render():
|
for _ in gui_app.render():
|
||||||
ui_state.update()
|
ui_state.update()
|
||||||
|
|
||||||
#TODO handle brigntness and awake state here
|
# TODO handle brigntness and awake state here
|
||||||
|
|
||||||
main_layout.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
main_layout.render()
|
||||||
|
|
||||||
kick_watchdog()
|
kick_watchdog()
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
import time
|
||||||
|
from collections.abc import Callable
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from cereal import messaging, log
|
from cereal import messaging, log
|
||||||
from openpilot.common.params import Params, UnknownKeyName
|
from openpilot.common.params import Params, UnknownKeyName
|
||||||
|
from openpilot.selfdrive.ui.lib.prime_state import PrimeState
|
||||||
|
|
||||||
UI_BORDER_SIZE = 30
|
UI_BORDER_SIZE = 30
|
||||||
|
|
||||||
@@ -44,6 +46,8 @@ class UIState:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.prime_state = PrimeState()
|
||||||
|
|
||||||
# UI Status tracking
|
# UI Status tracking
|
||||||
self.status: UIStatus = UIStatus.DISENGAGED
|
self.status: UIStatus = UIStatus.DISENGAGED
|
||||||
self.started_frame: int = 0
|
self.started_frame: int = 0
|
||||||
@@ -64,10 +68,17 @@ class UIState:
|
|||||||
def engaged(self) -> bool:
|
def engaged(self) -> bool:
|
||||||
return self.started and self.sm["selfdriveState"].enabled
|
return self.started and self.sm["selfdriveState"].enabled
|
||||||
|
|
||||||
|
def is_onroad(self) -> bool:
|
||||||
|
return self.started
|
||||||
|
|
||||||
|
def is_offroad(self) -> bool:
|
||||||
|
return not self.started
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
self.sm.update(0)
|
self.sm.update(0)
|
||||||
self._update_state()
|
self._update_state()
|
||||||
self._update_status()
|
self._update_status()
|
||||||
|
device.update()
|
||||||
|
|
||||||
def _update_state(self) -> None:
|
def _update_state(self) -> None:
|
||||||
# Handle panda states updates
|
# Handle panda states updates
|
||||||
@@ -125,5 +136,36 @@ class UIState:
|
|||||||
self.is_metric = False
|
self.is_metric = False
|
||||||
|
|
||||||
|
|
||||||
|
class Device:
|
||||||
|
def __init__(self):
|
||||||
|
self._ignition = False
|
||||||
|
self._interaction_time: float = 0.0
|
||||||
|
self._interactive_timeout_callbacks: list[Callable] = []
|
||||||
|
self._prev_timed_out = False
|
||||||
|
self.reset_interactive_timeout()
|
||||||
|
|
||||||
|
def reset_interactive_timeout(self, timeout: int = -1) -> None:
|
||||||
|
if timeout == -1:
|
||||||
|
timeout = 10 if ui_state.ignition else 30
|
||||||
|
self._interaction_time = time.monotonic() + timeout
|
||||||
|
|
||||||
|
def add_interactive_timeout_callback(self, callback: Callable):
|
||||||
|
self._interactive_timeout_callbacks.append(callback)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
# Handle interactive timeout
|
||||||
|
ignition_just_turned_off = not ui_state.ignition and self._ignition
|
||||||
|
self._ignition = ui_state.ignition
|
||||||
|
|
||||||
|
interaction_timeout = time.monotonic() > self._interaction_time
|
||||||
|
if ignition_just_turned_off or rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||||
|
self.reset_interactive_timeout()
|
||||||
|
elif interaction_timeout and not self._prev_timed_out:
|
||||||
|
for callback in self._interactive_timeout_callbacks:
|
||||||
|
callback()
|
||||||
|
self._prev_timed_out = interaction_timeout
|
||||||
|
|
||||||
|
|
||||||
# Global instance
|
# Global instance
|
||||||
ui_state = UIState()
|
ui_state = UIState()
|
||||||
|
device = Device()
|
||||||
|
|||||||
@@ -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.wrap_text import wrap_text
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
|
from openpilot.system.ui.lib.widget import Widget
|
||||||
|
|
||||||
|
|
||||||
class AlertColors:
|
class AlertColors:
|
||||||
HIGH_SEVERITY = rl.Color(226, 44, 44, 255)
|
HIGH_SEVERITY = rl.Color(226, 44, 44, 255)
|
||||||
@@ -40,8 +42,9 @@ class AlertData:
|
|||||||
visible: bool = False
|
visible: bool = False
|
||||||
|
|
||||||
|
|
||||||
class AbstractAlert(ABC):
|
class AbstractAlert(Widget, ABC):
|
||||||
def __init__(self, has_reboot_btn: bool = False):
|
def __init__(self, has_reboot_btn: bool = False):
|
||||||
|
super().__init__()
|
||||||
self.params = Params()
|
self.params = Params()
|
||||||
self.has_reboot_btn = has_reboot_btn
|
self.has_reboot_btn = has_reboot_btn
|
||||||
self.dismiss_callback: Callable | None = None
|
self.dismiss_callback: Callable | None = None
|
||||||
@@ -67,8 +70,7 @@ class AbstractAlert(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def handle_input(self, mouse_pos: rl.Vector2, mouse_clicked: bool) -> bool:
|
def handle_input(self, mouse_pos: rl.Vector2, mouse_clicked: bool) -> bool:
|
||||||
# TODO: fix scroll_panel.is_click_valid()
|
if not mouse_clicked or not self.scroll_panel.is_touch_valid():
|
||||||
if not mouse_clicked:
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if rl.check_collision_point_rec(mouse_pos, self.dismiss_btn_rect):
|
if rl.check_collision_point_rec(mouse_pos, self.dismiss_btn_rect):
|
||||||
@@ -88,7 +90,7 @@ class AbstractAlert(ABC):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def render(self, rect: rl.Rectangle):
|
def _render(self, rect: rl.Rectangle):
|
||||||
rl.draw_rectangle_rounded(rect, AlertConstants.BORDER_RADIUS / rect.width, 10, AlertColors.BACKGROUND)
|
rl.draw_rectangle_rounded(rect, AlertConstants.BORDER_RADIUS / rect.width, 10, AlertColors.BACKGROUND)
|
||||||
|
|
||||||
footer_height = AlertConstants.BUTTON_SIZE[1] + AlertConstants.SPACING
|
footer_height = AlertConstants.BUTTON_SIZE[1] + AlertConstants.SPACING
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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())
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.common.swaglog import cloudlog
|
||||||
|
|
||||||
|
|
||||||
|
class LagdToggle:
|
||||||
|
def __init__(self):
|
||||||
|
self.params = Params()
|
||||||
|
self.lag = 0.0
|
||||||
|
self._last_desc = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def software_delay(self):
|
||||||
|
return float(self.params.get("LagdToggledelay", encoding='utf8'))
|
||||||
|
|
||||||
|
def _maybe_update_desc(self, desc):
|
||||||
|
if desc != self._last_desc:
|
||||||
|
self.params.put_nonblocking("LagdToggleDesc", desc)
|
||||||
|
self._last_desc = desc
|
||||||
|
|
||||||
|
def lagd_main(self, CP, sm, model):
|
||||||
|
if self.params.get_bool("LagdToggle"):
|
||||||
|
lateral_delay = sm["liveDelay"].lateralDelay
|
||||||
|
lat_smooth = model.LAT_SMOOTH_SECONDS
|
||||||
|
result = lateral_delay + lat_smooth
|
||||||
|
desc = f"live steer delay learner ({lateral_delay:.3f}s) + model smoothing filter ({lat_smooth:.3f}s) = total delay ({result:.3f}s)"
|
||||||
|
self._maybe_update_desc(desc)
|
||||||
|
return result
|
||||||
|
|
||||||
|
steer_actuator_delay = CP.steerActuatorDelay
|
||||||
|
lat_smooth = model.LAT_SMOOTH_SECONDS
|
||||||
|
delay = self.software_delay
|
||||||
|
result = (steer_actuator_delay + delay) + lat_smooth
|
||||||
|
desc = (f"Vehicle steering delay ({steer_actuator_delay:.3f}s) + software delay ({delay:.3f}s) + " +
|
||||||
|
f"model smoothing filter ({lat_smooth:.3f}s) = total delay ({result:.3f}s)")
|
||||||
|
self._maybe_update_desc(desc)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def lagd_torqued_main(self, CP, msg):
|
||||||
|
if self.params.get_bool("LagdToggle"):
|
||||||
|
self.lag = msg.lateralDelay
|
||||||
|
cloudlog.debug(f"TORQUED USING LIVE DELAY: {self.lag:.3f}")
|
||||||
|
else:
|
||||||
|
self.lag = CP.steerActuatorDelay + self.software_delay
|
||||||
|
cloudlog.debug(f"TORQUED USING STEER ACTUATOR: {self.lag:.3f}")
|
||||||
|
return self.lag
|
||||||
@@ -18,7 +18,7 @@ from cereal import messaging
|
|||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.sunnypilot.mapd.mapd_manager import MAPD_PATH, MAPD_BIN_DIR
|
from openpilot.sunnypilot.mapd.mapd_manager import MAPD_PATH, MAPD_BIN_DIR
|
||||||
from openpilot.system.hardware.hw import Paths
|
from openpilot.system.hardware.hw import Paths
|
||||||
from openpilot.system.ui.spinner import Spinner
|
from openpilot.common.spinner import Spinner
|
||||||
from openpilot.system.version import is_prebuilt
|
from openpilot.system.version import is_prebuilt
|
||||||
import openpilot.system.sentry as sentry
|
import openpilot.system.sentry as sentry
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from openpilot.sunnypilot.modeld.parse_model_outputs import Parser
|
|||||||
from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState
|
from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState
|
||||||
from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan
|
from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan
|
||||||
from openpilot.sunnypilot.models.helpers import get_active_bundle, get_model_path, load_metadata, prepare_inputs, load_meta_constants
|
from openpilot.sunnypilot.models.helpers import get_active_bundle, get_model_path, load_metadata, prepare_inputs, load_meta_constants
|
||||||
from openpilot.sunnypilot.models.modeld_lagd import ModeldLagd
|
from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle
|
||||||
from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext
|
from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext
|
||||||
|
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ def main(demo=False):
|
|||||||
|
|
||||||
cloudlog.info("modeld got CarParams: %s", CP.brand)
|
cloudlog.info("modeld got CarParams: %s", CP.brand)
|
||||||
|
|
||||||
modeld_lagd = ModeldLagd()
|
modeld_lagd = LagdToggle()
|
||||||
|
|
||||||
# Enable lagd support for sunnypilot modeld
|
# Enable lagd support for sunnypilot modeld
|
||||||
long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS
|
long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFr
|
|||||||
from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants
|
from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants
|
||||||
|
|
||||||
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||||
from openpilot.sunnypilot.models.modeld_lagd import ModeldLagd
|
from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle
|
||||||
from openpilot.sunnypilot.models.runners.helpers import get_model_runner
|
from openpilot.sunnypilot.models.runners.helpers import get_model_runner
|
||||||
|
|
||||||
PROCESS_NAME = "selfdrive.modeld.modeld"
|
PROCESS_NAME = "selfdrive.modeld.modeld"
|
||||||
@@ -239,7 +239,7 @@ def main(demo=False):
|
|||||||
cloudlog.info("modeld got CarParams: %s", CP.brand)
|
cloudlog.info("modeld got CarParams: %s", CP.brand)
|
||||||
|
|
||||||
|
|
||||||
modeld_lagd = ModeldLagd()
|
modeld_lagd = LagdToggle()
|
||||||
|
|
||||||
# TODO Move smooth seconds to action function
|
# TODO Move smooth seconds to action function
|
||||||
long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS
|
long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
"""
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
from openpilot.common.params import Params
|
|
||||||
from openpilot.common.swaglog import cloudlog
|
|
||||||
|
|
||||||
|
|
||||||
class ModeldLagd:
|
|
||||||
def __init__(self):
|
|
||||||
self.params = Params()
|
|
||||||
|
|
||||||
def lagd_main(self, CP, sm, model):
|
|
||||||
if self.params.get_bool("LagdToggle"):
|
|
||||||
lateral_delay = sm["liveDelay"].lateralDelay
|
|
||||||
lat_smooth = model.LAT_SMOOTH_SECONDS
|
|
||||||
result = lateral_delay + lat_smooth
|
|
||||||
cloudlog.debug(f"LAGD USING LIVE DELAY: {lateral_delay:.3f} + {lat_smooth:.3f} = {result:.3f}")
|
|
||||||
return result
|
|
||||||
steer_actuator_delay = CP.steerActuatorDelay
|
|
||||||
lat_smooth = model.LAT_SMOOTH_SECONDS
|
|
||||||
result = (steer_actuator_delay + 0.2) + lat_smooth
|
|
||||||
cloudlog.debug(f"LAGD USING STEER ACTUATOR: {steer_actuator_delay:.3f} + 0.2 + {lat_smooth:.3f} = {result:.3f}")
|
|
||||||
return result
|
|
||||||
@@ -1 +1 @@
|
|||||||
dec34c57c4131f6fca5d1035201d1afbf43e5250cede7bfdc798371af008afad
|
71979b29c4bab3007de1a4265442d79f44c0eaef066af66086dddfc432709b94
|
||||||
@@ -1,25 +1,17 @@
|
|||||||
class WMACConstants:
|
class WMACConstants:
|
||||||
LEAD_WINDOW_SIZE = 5
|
# Lead detection parameters
|
||||||
LEAD_PROB = 0.5
|
LEAD_WINDOW_SIZE = 6 # Stable detection window
|
||||||
|
LEAD_PROB = 0.45 # Balanced threshold for lead detection
|
||||||
|
|
||||||
SLOW_DOWN_WINDOW_SIZE = 5
|
# Slow down detection parameters
|
||||||
SLOW_DOWN_PROB = 0.6
|
SLOW_DOWN_WINDOW_SIZE = 5 # Responsive but stable
|
||||||
|
SLOW_DOWN_PROB = 0.3 # Balanced threshold for slow down scenarios
|
||||||
|
|
||||||
|
# Optimized slow down distance curve - smooth and progressive
|
||||||
SLOW_DOWN_BP = [0., 10., 20., 30., 40., 50., 55., 60.]
|
SLOW_DOWN_BP = [0., 10., 20., 30., 40., 50., 55., 60.]
|
||||||
SLOW_DOWN_DIST = [25., 38., 55., 75., 95., 115., 130., 150.]
|
SLOW_DOWN_DIST = [32., 46., 64., 86., 108., 130., 145., 165.]
|
||||||
|
|
||||||
SLOWNESS_WINDOW_SIZE = 12
|
# Slowness detection parameters
|
||||||
SLOWNESS_PROB = 0.5
|
SLOWNESS_WINDOW_SIZE = 10 # Stable slowness detection
|
||||||
SLOWNESS_CRUISE_OFFSET = 1.05
|
SLOWNESS_PROB = 0.55 # Clear threshold for slowness
|
||||||
|
SLOWNESS_CRUISE_OFFSET = 1.025 # Conservative cruise speed offset
|
||||||
DANGEROUS_TTC_WINDOW_SIZE = 3
|
|
||||||
DANGEROUS_TTC = 2.3
|
|
||||||
|
|
||||||
MPC_FCW_WINDOW_SIZE = 10
|
|
||||||
MPC_FCW_PROB = 0.5
|
|
||||||
|
|
||||||
|
|
||||||
class SNG_State:
|
|
||||||
off = 0
|
|
||||||
stopped = 1
|
|
||||||
going = 2
|
|
||||||
|
|||||||
@@ -1,88 +1,133 @@
|
|||||||
# The MIT License
|
"""
|
||||||
#
|
Copyright (c) 2021-, rav4kumar, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||||
# Copyright (c) 2019-, Rick Lan, dragonpilot community, and a number of other of contributors.
|
|
||||||
#
|
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
# of this software and associated documentation files (the "Software"), to deal
|
|
||||||
# in the Software without restriction, including without limitation the rights
|
|
||||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
# copies of the Software, and to permit persons to whom the Software is
|
|
||||||
# furnished to do so, subject to the following conditions:
|
|
||||||
#
|
|
||||||
# The above copyright notice and this permission notice shall be included in
|
|
||||||
# all copies or substantial portions of the Software.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
# THE SOFTWARE.
|
|
||||||
#
|
|
||||||
# Version = 2025-1-18
|
|
||||||
|
|
||||||
import numpy as np
|
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.
|
||||||
|
"""
|
||||||
|
# Version = 2025-6-30
|
||||||
|
|
||||||
from cereal import messaging
|
from cereal import messaging
|
||||||
from opendbc.car import structs
|
from opendbc.car import structs
|
||||||
from numpy import interp
|
from numpy import interp
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.common.realtime import DT_MDL
|
from openpilot.common.realtime import DT_MDL
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants, SNG_State
|
from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
# d-e2e, from modeldata.h
|
# d-e2e, from modeldata.h
|
||||||
TRAJECTORY_SIZE = 33
|
TRAJECTORY_SIZE = 33
|
||||||
|
SET_MODE_TIMEOUT = 15
|
||||||
|
|
||||||
HIGHWAY_CRUISE_KPH = 70
|
# Define the valid mode types
|
||||||
|
ModeType = Literal['acc', 'blended']
|
||||||
STOP_AND_GO_FRAME = 60
|
|
||||||
|
|
||||||
SET_MODE_TIMEOUT = 10
|
|
||||||
|
|
||||||
V_ACC_MIN = 9.72
|
|
||||||
|
|
||||||
|
|
||||||
class GenericMovingAverageCalculator:
|
class SmoothKalmanFilter:
|
||||||
def __init__(self, window_size):
|
"""Enhanced Kalman filter with smoothing for stable decision making."""
|
||||||
self.window_size = window_size
|
|
||||||
self.data = []
|
|
||||||
self.total = 0
|
|
||||||
|
|
||||||
def add_data(self, value: float) -> None:
|
def __init__(self, initial_value=0, measurement_noise=0.1, process_noise=0.01,
|
||||||
if len(self.data) == self.window_size:
|
alpha=1.0, smoothing_factor=0.85):
|
||||||
self.total -= self.data.pop(0)
|
self.x = initial_value
|
||||||
self.data.append(value)
|
self.P = 1.0
|
||||||
self.total += value
|
self.R = measurement_noise
|
||||||
|
self.Q = process_noise
|
||||||
|
self.alpha = alpha
|
||||||
|
self.smoothing_factor = smoothing_factor
|
||||||
|
self.initialized = False
|
||||||
|
self.history = []
|
||||||
|
self.max_history = 10
|
||||||
|
self.confidence = 0.0
|
||||||
|
|
||||||
def get_moving_average(self) -> float | None:
|
def add_data(self, measurement):
|
||||||
return None if len(self.data) == 0 else self.total / len(self.data)
|
if len(self.history) >= self.max_history:
|
||||||
|
self.history.pop(0)
|
||||||
|
self.history.append(measurement)
|
||||||
|
|
||||||
def reset_data(self) -> None:
|
if not self.initialized:
|
||||||
self.data = []
|
self.x = measurement
|
||||||
self.total = 0
|
self.initialized = True
|
||||||
|
self.confidence = 0.1
|
||||||
|
return
|
||||||
|
|
||||||
|
self.P = self.alpha * self.P + self.Q
|
||||||
|
|
||||||
|
K = self.P / (self.P + self.R)
|
||||||
|
effective_K = K * (1.0 - self.smoothing_factor) + self.smoothing_factor * 0.1
|
||||||
|
|
||||||
|
innovation = measurement - self.x
|
||||||
|
self.x = self.x + effective_K * innovation
|
||||||
|
self.P = (1 - effective_K) * self.P
|
||||||
|
|
||||||
|
if abs(innovation) < 0.1:
|
||||||
|
self.confidence = min(1.0, self.confidence + 0.05)
|
||||||
|
else:
|
||||||
|
self.confidence = max(0.1, self.confidence - 0.02)
|
||||||
|
|
||||||
|
def get_value(self):
|
||||||
|
return self.x if self.initialized else None
|
||||||
|
|
||||||
|
def get_confidence(self):
|
||||||
|
return self.confidence
|
||||||
|
|
||||||
|
def reset_data(self):
|
||||||
|
self.initialized = False
|
||||||
|
self.history = []
|
||||||
|
self.confidence = 0.0
|
||||||
|
|
||||||
|
|
||||||
class WeightedMovingAverageCalculator:
|
class ModeTransitionManager:
|
||||||
def __init__(self, window_size):
|
"""Manages smooth transitions between driving modes with hysteresis."""
|
||||||
self.window_size = window_size
|
|
||||||
self.data = []
|
|
||||||
self.weights = np.linspace(1, 3, window_size) # Linear weights, adjust as needed
|
|
||||||
|
|
||||||
def add_data(self, value: float) -> None:
|
def __init__(self):
|
||||||
if len(self.data) == self.window_size:
|
self.current_mode: ModeType = 'acc'
|
||||||
self.data.pop(0)
|
self.mode_confidence = {'acc': 1.0, 'blended': 0.0}
|
||||||
self.data.append(value)
|
self.transition_timeout = 0
|
||||||
|
self.min_mode_duration = 10
|
||||||
|
self.mode_duration = 0
|
||||||
|
self.emergency_override = False
|
||||||
|
|
||||||
def get_weighted_average(self) -> float | None:
|
def request_mode(self, mode: ModeType, confidence: float = 1.0, emergency: bool = False):
|
||||||
if len(self.data) == 0:
|
# Emergency override for critical situations (stops, collisions)
|
||||||
return None
|
if emergency:
|
||||||
weighted_sum: float = float(np.dot(self.data, self.weights[-len(self.data):]))
|
self.emergency_override = True
|
||||||
weight_total: float = float(np.sum(self.weights[-len(self.data):]))
|
self.current_mode = mode
|
||||||
return weighted_sum / weight_total
|
self.transition_timeout = SET_MODE_TIMEOUT
|
||||||
|
self.mode_duration = 0
|
||||||
|
return
|
||||||
|
|
||||||
def reset_data(self) -> None:
|
self.mode_confidence[mode] = min(1.0, self.mode_confidence[mode] + 0.1 * confidence)
|
||||||
self.data = []
|
for m in self.mode_confidence:
|
||||||
|
if m != mode:
|
||||||
|
self.mode_confidence[m] = max(0.0, self.mode_confidence[m] - 0.05)
|
||||||
|
|
||||||
|
# Require minimum duration in current mode (unless emergency)
|
||||||
|
if self.mode_duration < self.min_mode_duration and not self.emergency_override:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Hysteresis: higher threshold for mode changes
|
||||||
|
confidence_threshold = 0.6 if mode != self.current_mode else 0.3 # Lower threshold for faster response
|
||||||
|
|
||||||
|
if self.mode_confidence[mode] > confidence_threshold:
|
||||||
|
if mode != self.current_mode and self.transition_timeout == 0:
|
||||||
|
self.transition_timeout = SET_MODE_TIMEOUT
|
||||||
|
self.current_mode = mode
|
||||||
|
self.mode_duration = 0
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
if self.transition_timeout > 0:
|
||||||
|
self.transition_timeout -= 1
|
||||||
|
self.mode_duration += 1
|
||||||
|
|
||||||
|
# Reset emergency override after some time
|
||||||
|
if self.emergency_override and self.mode_duration > 20:
|
||||||
|
self.emergency_override = False
|
||||||
|
|
||||||
|
# Gradual confidence decay
|
||||||
|
for mode in self.mode_confidence:
|
||||||
|
self.mode_confidence[mode] *= 0.98
|
||||||
|
|
||||||
|
def get_mode(self) -> ModeType:
|
||||||
|
return self.current_mode
|
||||||
|
|
||||||
|
|
||||||
class DynamicExperimentalController:
|
class DynamicExperimentalController:
|
||||||
@@ -92,51 +137,59 @@ class DynamicExperimentalController:
|
|||||||
self._params = params or Params()
|
self._params = params or Params()
|
||||||
self._enabled: bool = self._params.get_bool("DynamicExperimentalControl")
|
self._enabled: bool = self._params.get_bool("DynamicExperimentalControl")
|
||||||
self._active: bool = False
|
self._active: bool = False
|
||||||
self._mode: str = 'acc'
|
|
||||||
self._frame: int = 0
|
self._frame: int = 0
|
||||||
|
self._urgency = 0.0
|
||||||
|
|
||||||
# Use weighted moving average for filtering leads
|
self._mode_manager = ModeTransitionManager()
|
||||||
self._lead_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.LEAD_WINDOW_SIZE)
|
|
||||||
|
# Smooth filters for stable decision making with faster response for critical scenarios
|
||||||
|
self._lead_filter = SmoothKalmanFilter(
|
||||||
|
measurement_noise=0.15,
|
||||||
|
process_noise=0.05,
|
||||||
|
alpha=1.02,
|
||||||
|
smoothing_factor=0.8
|
||||||
|
)
|
||||||
|
|
||||||
|
self._slow_down_filter = SmoothKalmanFilter(
|
||||||
|
measurement_noise=0.1,
|
||||||
|
process_noise=0.1,
|
||||||
|
alpha=1.05,
|
||||||
|
smoothing_factor=0.7
|
||||||
|
)
|
||||||
|
|
||||||
|
self._slowness_filter = SmoothKalmanFilter(
|
||||||
|
measurement_noise=0.1,
|
||||||
|
process_noise=0.06,
|
||||||
|
alpha=1.015,
|
||||||
|
smoothing_factor=0.92
|
||||||
|
)
|
||||||
|
|
||||||
|
self._mpc_fcw_filter = SmoothKalmanFilter(
|
||||||
|
measurement_noise=0.2,
|
||||||
|
process_noise=0.1,
|
||||||
|
alpha=1.1,
|
||||||
|
smoothing_factor=0.5
|
||||||
|
)
|
||||||
self._has_lead_filtered = False
|
self._has_lead_filtered = False
|
||||||
self._has_lead_filtered_prev = False
|
self._has_slow_down = False
|
||||||
|
self._has_slowness = False
|
||||||
self._slow_down_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.SLOW_DOWN_WINDOW_SIZE)
|
self._has_mpc_fcw = False
|
||||||
self._has_slow_down: bool = False
|
self._v_ego_kph = 0.0
|
||||||
self._slow_down_confidence: float = 0.0
|
self._v_cruise_kph = 0.0
|
||||||
|
|
||||||
self._has_blinkers = False
|
|
||||||
|
|
||||||
self._slowness_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.SLOWNESS_WINDOW_SIZE)
|
|
||||||
self._has_slowness: bool = False
|
|
||||||
|
|
||||||
self._has_nav_instruction = False
|
|
||||||
|
|
||||||
self._dangerous_ttc_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.DANGEROUS_TTC_WINDOW_SIZE)
|
|
||||||
self._has_dangerous_ttc: bool = False
|
|
||||||
|
|
||||||
self._v_ego_kph = 0.
|
|
||||||
self._v_cruise_kph = 0.
|
|
||||||
|
|
||||||
self._has_lead = False
|
|
||||||
|
|
||||||
self._has_standstill = False
|
self._has_standstill = False
|
||||||
self._has_standstill_prev = False
|
|
||||||
|
|
||||||
self._sng_transit_frame = 0
|
|
||||||
self._sng_state = SNG_State.off
|
|
||||||
|
|
||||||
self._mpc_fcw_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.MPC_FCW_WINDOW_SIZE)
|
|
||||||
self._has_mpc_fcw: bool = False
|
|
||||||
self._mpc_fcw_crash_cnt = 0
|
self._mpc_fcw_crash_cnt = 0
|
||||||
|
self._standstill_count = 0
|
||||||
self._set_mode_timeout = 0
|
# debug
|
||||||
|
self._endpoint_x = float('inf')
|
||||||
|
self._expected_distance = 0.0
|
||||||
|
self._trajectory_valid = False
|
||||||
|
|
||||||
def _read_params(self) -> None:
|
def _read_params(self) -> None:
|
||||||
if self._frame % int(1. / DT_MDL) == 0:
|
if self._frame % int(1. / DT_MDL) == 0:
|
||||||
self._enabled = self._params.get_bool("DynamicExperimentalControl")
|
self._enabled = self._params.get_bool("DynamicExperimentalControl")
|
||||||
|
|
||||||
def mode(self) -> str:
|
def mode(self) -> str:
|
||||||
return str(self._mode)
|
return self._mode_manager.get_mode()
|
||||||
|
|
||||||
def enabled(self) -> bool:
|
def enabled(self) -> bool:
|
||||||
return self._enabled
|
return self._enabled
|
||||||
@@ -144,46 +197,9 @@ class DynamicExperimentalController:
|
|||||||
def active(self) -> bool:
|
def active(self) -> bool:
|
||||||
return self._active
|
return self._active
|
||||||
|
|
||||||
@staticmethod
|
def set_mpc_fcw_crash_cnt(self) -> None:
|
||||||
def _anomaly_detection(recent_data: list[float], threshold: float = 2.0, context_check: bool = True) -> bool:
|
"""Set MPC FCW crash count"""
|
||||||
"""
|
self._mpc_fcw_crash_cnt = self._mpc.crash_cnt
|
||||||
Basic anomaly detection using standard deviation.
|
|
||||||
"""
|
|
||||||
if len(recent_data) < 5:
|
|
||||||
return False
|
|
||||||
mean: float = float(np.mean(recent_data))
|
|
||||||
std_dev: float = float(np.std(recent_data))
|
|
||||||
anomaly: bool = bool(recent_data[-1] > mean + threshold * std_dev)
|
|
||||||
|
|
||||||
# Context check to ensure repeated anomaly
|
|
||||||
if context_check:
|
|
||||||
return np.count_nonzero(np.array(recent_data) > mean + threshold * std_dev) > 1
|
|
||||||
return anomaly
|
|
||||||
|
|
||||||
def _adaptive_slowdown_threshold(self) -> float:
|
|
||||||
"""
|
|
||||||
Adapts the slow-down threshold based on vehicle speed and recent behavior.
|
|
||||||
"""
|
|
||||||
slowdown_scaling_factor: float = (1.0 + 0.03 * np.log(1 + len(self._slow_down_gmac.data)))
|
|
||||||
adaptive_threshold: float = float(
|
|
||||||
interp(self._v_ego_kph, WMACConstants.SLOW_DOWN_BP, WMACConstants.SLOW_DOWN_DIST) * slowdown_scaling_factor
|
|
||||||
)
|
|
||||||
return adaptive_threshold
|
|
||||||
|
|
||||||
def _smoothed_lead_detection(self, lead_prob: float, smoothing_factor: float = 0.2):
|
|
||||||
"""
|
|
||||||
Smoothing the lead detection to avoid erratic behavior.
|
|
||||||
"""
|
|
||||||
lead_filtering: float = (1 - smoothing_factor) * self._has_lead_filtered + smoothing_factor * lead_prob
|
|
||||||
return lead_filtering > WMACConstants.LEAD_PROB
|
|
||||||
|
|
||||||
def _adaptive_lead_prob_threshold(self) -> float:
|
|
||||||
"""
|
|
||||||
Adapts lead probability threshold based on driving conditions.
|
|
||||||
"""
|
|
||||||
if self._v_ego_kph > HIGHWAY_CRUISE_KPH:
|
|
||||||
return float(WMACConstants.LEAD_PROB + 0.1) # Increase the threshold on highways
|
|
||||||
return float(WMACConstants.LEAD_PROB)
|
|
||||||
|
|
||||||
def _update_calculations(self, sm: messaging.SubMaster) -> None:
|
def _update_calculations(self, sm: messaging.SubMaster) -> None:
|
||||||
car_state = sm['carState']
|
car_state = sm['carState']
|
||||||
@@ -192,186 +208,168 @@ class DynamicExperimentalController:
|
|||||||
|
|
||||||
self._v_ego_kph = car_state.vEgo * 3.6
|
self._v_ego_kph = car_state.vEgo * 3.6
|
||||||
self._v_cruise_kph = car_state.vCruise
|
self._v_cruise_kph = car_state.vCruise
|
||||||
self._has_lead = lead_one.status
|
|
||||||
self._has_standstill = car_state.standstill
|
self._has_standstill = car_state.standstill
|
||||||
|
|
||||||
# fcw detection
|
# standstill detection
|
||||||
self._mpc_fcw_gmac.add_data(self._mpc_fcw_crash_cnt > 0)
|
|
||||||
if _mpc_fcw_weighted_average := self._mpc_fcw_gmac.get_weighted_average():
|
|
||||||
self._has_mpc_fcw = _mpc_fcw_weighted_average > WMACConstants.MPC_FCW_PROB
|
|
||||||
else:
|
|
||||||
self._has_mpc_fcw = False
|
|
||||||
|
|
||||||
# nav enable detection
|
|
||||||
# self._has_nav_instruction = md.navEnabledDEPRECATED and maneuver_distance / max(car_state.vEgo, 1) < 13
|
|
||||||
|
|
||||||
# lead detection with smoothing
|
|
||||||
self._lead_gmac.add_data(lead_one.status)
|
|
||||||
self._has_lead_filtered = (self._lead_gmac.get_weighted_average() or -1.) > WMACConstants.LEAD_PROB
|
|
||||||
#lead_prob = self._lead_gmac.get_weighted_average() or 0
|
|
||||||
#self._has_lead_filtered = self._smoothed_lead_detection(lead_prob)
|
|
||||||
|
|
||||||
# adaptive slow down detection
|
|
||||||
adaptive_threshold = self._adaptive_slowdown_threshold()
|
|
||||||
slow_down_trigger = len(md.orientation.x) == len(md.position.x) == TRAJECTORY_SIZE and md.position.x[TRAJECTORY_SIZE - 1] < adaptive_threshold
|
|
||||||
self._slow_down_gmac.add_data(slow_down_trigger)
|
|
||||||
if _has_slow_down_weighted_average := self._slow_down_gmac.get_weighted_average():
|
|
||||||
self._has_slow_down = _has_slow_down_weighted_average > WMACConstants.SLOW_DOWN_PROB
|
|
||||||
self._slow_down_confidence = _has_slow_down_weighted_average # Store confidence level
|
|
||||||
else:
|
|
||||||
self._has_slow_down = False
|
|
||||||
self._slow_down_confidence = 0.0 # No confidence if no slowdown
|
|
||||||
|
|
||||||
# anomaly detection for slow down events
|
|
||||||
if self._anomaly_detection(self._slow_down_gmac.data):
|
|
||||||
self._slow_down_confidence *= 0.85 # Reduce confidence
|
|
||||||
self._has_slow_down = self._slow_down_confidence > WMACConstants.SLOW_DOWN_PROB
|
|
||||||
|
|
||||||
# blinker detection
|
|
||||||
self._has_blinkers = car_state.leftBlinker or car_state.rightBlinker
|
|
||||||
|
|
||||||
# sng detection
|
|
||||||
if self._has_standstill:
|
if self._has_standstill:
|
||||||
self._sng_state = SNG_State.stopped
|
self._standstill_count = min(20, self._standstill_count + 1)
|
||||||
self._sng_transit_frame = 0
|
|
||||||
else:
|
else:
|
||||||
if self._sng_transit_frame == 0:
|
self._standstill_count = max(0, self._standstill_count - 1)
|
||||||
if self._sng_state == SNG_State.stopped:
|
|
||||||
self._sng_state = SNG_State.going
|
|
||||||
self._sng_transit_frame = STOP_AND_GO_FRAME
|
|
||||||
elif self._sng_state == SNG_State.going:
|
|
||||||
self._sng_state = SNG_State.off
|
|
||||||
elif self._sng_transit_frame > 0:
|
|
||||||
self._sng_transit_frame -= 1
|
|
||||||
|
|
||||||
# slowness detection
|
# Lead detection
|
||||||
if not self._has_standstill:
|
self._lead_filter.add_data(float(lead_one.status))
|
||||||
self._slowness_gmac.add_data(self._v_ego_kph <= (self._v_cruise_kph * WMACConstants.SLOWNESS_CRUISE_OFFSET))
|
lead_value = self._lead_filter.get_value() or 0.0
|
||||||
if _slowness_weighted_average := self._slowness_gmac.get_weighted_average():
|
self._has_lead_filtered = lead_value > WMACConstants.LEAD_PROB
|
||||||
self._has_slowness = _slowness_weighted_average > WMACConstants.SLOWNESS_PROB
|
|
||||||
else:
|
|
||||||
self._has_slowness = False
|
|
||||||
|
|
||||||
# dangerous TTC detection
|
# MPC FCW detection
|
||||||
if not self._has_lead_filtered and self._has_lead_filtered_prev:
|
fcw_filtered_value = self._mpc_fcw_filter.get_value() or 0.0
|
||||||
self._dangerous_ttc_gmac.reset_data()
|
self._mpc_fcw_filter.add_data(float(self._mpc_fcw_crash_cnt > 0))
|
||||||
self._has_dangerous_ttc = False
|
self._has_mpc_fcw = fcw_filtered_value > 0.5
|
||||||
|
|
||||||
if self._has_lead and car_state.vEgo >= 0.01:
|
# Slow down detection
|
||||||
self._dangerous_ttc_gmac.add_data(lead_one.dRel / car_state.vEgo)
|
self._calculate_slow_down(md)
|
||||||
|
|
||||||
if _dangerous_ttc_weighted_average := self._dangerous_ttc_gmac.get_weighted_average():
|
# Slowness detection
|
||||||
self._has_dangerous_ttc = _dangerous_ttc_weighted_average <= WMACConstants.DANGEROUS_TTC
|
if not (self._standstill_count > 5) and not self._has_slow_down:
|
||||||
else:
|
current_slowness = float(self._v_ego_kph <= (self._v_cruise_kph * WMACConstants.SLOWNESS_CRUISE_OFFSET))
|
||||||
self._has_dangerous_ttc = False
|
self._slowness_filter.add_data(current_slowness)
|
||||||
|
slowness_value = self._slowness_filter.get_value() or 0.0
|
||||||
|
|
||||||
# keep prev values
|
# Hysteresis for slowness
|
||||||
self._has_standstill_prev = self._has_standstill
|
threshold = WMACConstants.SLOWNESS_PROB * (0.8 if self._has_slowness else 1.1)
|
||||||
self._has_lead_filtered_prev = self._has_lead_filtered
|
self._has_slowness = slowness_value > threshold
|
||||||
|
|
||||||
|
def _calculate_slow_down(self, md):
|
||||||
|
"""Calculate urgency based on trajectory endpoint vs expected distance."""
|
||||||
|
|
||||||
|
# Reset to safe defaults
|
||||||
|
urgency = 0.0
|
||||||
|
self._endpoint_x = float('inf')
|
||||||
|
self._trajectory_valid = False
|
||||||
|
|
||||||
|
#Require exact trajectory size
|
||||||
|
position_valid = len(md.position.x) == TRAJECTORY_SIZE
|
||||||
|
orientation_valid = len(md.orientation.x) == TRAJECTORY_SIZE
|
||||||
|
|
||||||
|
if not (position_valid and orientation_valid):
|
||||||
|
# Invalid trajectory - this itself might indicate a stop scenario
|
||||||
|
# Apply moderate urgency for incomplete trajectories at speed
|
||||||
|
if self._v_ego_kph > 20.0:
|
||||||
|
urgency = 0.3
|
||||||
|
|
||||||
|
self._slow_down_filter.add_data(urgency)
|
||||||
|
urgency_filtered = self._slow_down_filter.get_value() or 0.0
|
||||||
|
self._has_slow_down = urgency_filtered > WMACConstants.SLOW_DOWN_PROB
|
||||||
|
self._urgency = urgency_filtered
|
||||||
|
return
|
||||||
|
|
||||||
|
# We have a valid full trajectory
|
||||||
|
self._trajectory_valid = True
|
||||||
|
|
||||||
|
# Use the exact endpoint (33rd point, index 32)
|
||||||
|
endpoint_x = md.position.x[TRAJECTORY_SIZE - 1]
|
||||||
|
self._endpoint_x = endpoint_x
|
||||||
|
|
||||||
|
# Get expected distance based on current speed using tuned constants
|
||||||
|
expected_distance = interp(self._v_ego_kph,
|
||||||
|
WMACConstants.SLOW_DOWN_BP,
|
||||||
|
WMACConstants.SLOW_DOWN_DIST)
|
||||||
|
self._expected_distance = expected_distance
|
||||||
|
|
||||||
|
# Calculate urgency based on trajectory shortage
|
||||||
|
if endpoint_x < expected_distance:
|
||||||
|
shortage = expected_distance - endpoint_x
|
||||||
|
shortage_ratio = shortage / expected_distance
|
||||||
|
|
||||||
|
# Base urgency on shortage ratio
|
||||||
|
urgency = min(1.0, shortage_ratio * 2.0)
|
||||||
|
|
||||||
|
# Increase urgency for very short trajectories (imminent stops)
|
||||||
|
critical_distance = expected_distance * 0.3
|
||||||
|
if endpoint_x < critical_distance:
|
||||||
|
urgency = min(1.0, urgency * 2.0)
|
||||||
|
|
||||||
|
# Speed-based urgency adjustment
|
||||||
|
if self._v_ego_kph > 25.0:
|
||||||
|
speed_factor = 1.0 + (self._v_ego_kph - 25.0) / 80.0
|
||||||
|
urgency = min(1.0, urgency * speed_factor)
|
||||||
|
|
||||||
|
# Apply filtering but with less smoothing for stops
|
||||||
|
self._slow_down_filter.add_data(urgency)
|
||||||
|
urgency_filtered = self._slow_down_filter.get_value() or 0.0
|
||||||
|
|
||||||
|
# Update state with lower threshold for better stop detection
|
||||||
|
self._has_slow_down = urgency_filtered > (WMACConstants.SLOW_DOWN_PROB * 0.8)
|
||||||
|
self._urgency = urgency_filtered
|
||||||
|
|
||||||
def _radarless_mode(self) -> None:
|
def _radarless_mode(self) -> None:
|
||||||
# when mpc fcw crash prob is high
|
"""Radarless mode decision logic with emergency handling."""
|
||||||
# use blended to slow down quickly
|
|
||||||
|
# EMERGENCY: MPC FCW - immediate blended mode
|
||||||
if self._has_mpc_fcw:
|
if self._has_mpc_fcw:
|
||||||
self._set_mode('blended')
|
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Nav enabled and distance to upcoming turning is 300 or below
|
# Standstill: use blended
|
||||||
# if self._has_nav_instruction:
|
if self._standstill_count > 3:
|
||||||
# self._set_mode('blended')
|
self._mode_manager.request_mode('blended', confidence=0.9)
|
||||||
# return
|
|
||||||
|
|
||||||
# when blinker is on and speed is driving below V_ACC_MIN: blended
|
|
||||||
# we don't want it to switch mode at higher speed, blended may trigger hard brake
|
|
||||||
# if self._has_blinkers and self._v_ego_kph < V_ACC_MIN:
|
|
||||||
# self._set_mode('blended')
|
|
||||||
# return
|
|
||||||
|
|
||||||
# when at highway cruise and SNG: blended
|
|
||||||
# ensuring blended mode is used because acc is bad at catching SNG lead car
|
|
||||||
# especially those who accel very fast and then brake very hard.
|
|
||||||
# if self._sng_state == SNG_State.going and self._v_cruise_kph >= V_ACC_MIN:
|
|
||||||
# self._set_mode('blended')
|
|
||||||
# return
|
|
||||||
|
|
||||||
# when standstill: blended
|
|
||||||
# in case of lead car suddenly move away under traffic light, acc mode won't brake at traffic light.
|
|
||||||
if self._has_standstill:
|
|
||||||
self._set_mode('blended')
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# when detecting slow down scenario: blended
|
# Slow down scenarios: emergency for high urgency, normal for lower urgency
|
||||||
# e.g. traffic light, curve, stop sign etc.
|
|
||||||
if self._has_slow_down:
|
if self._has_slow_down:
|
||||||
self._set_mode('blended')
|
if self._urgency > 0.7:
|
||||||
|
# Emergency: immediate blended mode for high urgency stops
|
||||||
|
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
|
||||||
|
else:
|
||||||
|
# Normal: blended with urgency-based confidence
|
||||||
|
confidence = min(1.0, self._urgency * 1.5)
|
||||||
|
self._mode_manager.request_mode('blended', confidence=confidence)
|
||||||
return
|
return
|
||||||
|
|
||||||
# when detecting lead slow down: blended
|
# Driving slow: use ACC (but not if actively slowing down)
|
||||||
# use blended for higher braking capability
|
if self._has_slowness and not self._has_slow_down:
|
||||||
if self._has_dangerous_ttc:
|
self._mode_manager.request_mode('acc', confidence=0.8)
|
||||||
self._set_mode('blended')
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# car driving at speed lower than set speed: acc
|
# Default: ACC
|
||||||
if self._has_slowness:
|
self._mode_manager.request_mode('acc', confidence=0.7)
|
||||||
self._set_mode('acc')
|
|
||||||
return
|
|
||||||
|
|
||||||
self._set_mode('acc')
|
|
||||||
|
|
||||||
def _radar_mode(self) -> None:
|
def _radar_mode(self) -> None:
|
||||||
# when mpc fcw crash prob is high
|
"""Radar mode with emergency handling."""
|
||||||
# use blended to slow down quickly
|
|
||||||
|
# EMERGENCY: MPC FCW - immediate blended mode
|
||||||
if self._has_mpc_fcw:
|
if self._has_mpc_fcw:
|
||||||
self._set_mode('blended')
|
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
# If there is a filtered lead, the vehicle is not in standstill, and the lead vehicle's yRel meets the condition,
|
# If lead detected and not in standstill: always use ACC
|
||||||
if self._has_lead_filtered and not self._has_standstill:
|
if self._has_lead_filtered and not (self._standstill_count > 3):
|
||||||
self._set_mode('acc')
|
self._mode_manager.request_mode('acc', confidence=1.0)
|
||||||
return
|
return
|
||||||
|
|
||||||
# when blinker is on and speed is driving below V_ACC_MIN: blended
|
# Slow down scenarios: emergency for high urgency, normal for lower urgency
|
||||||
# we don't want it to switch mode at higher speed, blended may trigger hard brake
|
|
||||||
# if self._has_blinkers and self._v_ego_kph < V_ACC_MIN:
|
|
||||||
# self._set_mode('blended')
|
|
||||||
# return
|
|
||||||
|
|
||||||
# when standstill: blended
|
|
||||||
# in case of lead car suddenly move away under traffic light, acc mode won't brake at traffic light.
|
|
||||||
if self._has_standstill:
|
|
||||||
self._set_mode('blended')
|
|
||||||
return
|
|
||||||
|
|
||||||
# when detecting slow down scenario: blended
|
|
||||||
# e.g. traffic light, curve, stop sign etc.
|
|
||||||
if self._has_slow_down:
|
if self._has_slow_down:
|
||||||
self._set_mode('blended')
|
if self._urgency > 0.7:
|
||||||
|
# Emergency: immediate blended mode for high urgency stops
|
||||||
|
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
|
||||||
|
else:
|
||||||
|
# Normal: blended with urgency-based confidence
|
||||||
|
confidence = min(1.0, self._urgency * 1.3)
|
||||||
|
self._mode_manager.request_mode('blended', confidence=confidence)
|
||||||
return
|
return
|
||||||
|
|
||||||
# car driving at speed lower than set speed: acc
|
# Standstill: use blended
|
||||||
if self._has_slowness:
|
if self._standstill_count > 3:
|
||||||
self._set_mode('acc')
|
self._mode_manager.request_mode('blended', confidence=0.9)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Nav enabled and distance to upcoming turning is 300 or below
|
# Driving slow: use ACC (but not if actively slowing down)
|
||||||
# if self._has_nav_instruction:
|
if self._has_slowness and not self._has_slow_down:
|
||||||
# self._set_mode('blended')
|
self._mode_manager.request_mode('acc', confidence=0.8)
|
||||||
# return
|
return
|
||||||
|
|
||||||
self._set_mode('acc')
|
# Default: ACC
|
||||||
|
self._mode_manager.request_mode('acc', confidence=0.7)
|
||||||
def set_mpc_fcw_crash_cnt(self) -> None:
|
|
||||||
self._mpc_fcw_crash_cnt = self._mpc.crash_cnt
|
|
||||||
|
|
||||||
def _set_mode(self, mode: str) -> None:
|
|
||||||
if self._set_mode_timeout == 0:
|
|
||||||
self._mode = mode
|
|
||||||
if mode == 'blended':
|
|
||||||
self._set_mode_timeout = SET_MODE_TIMEOUT
|
|
||||||
|
|
||||||
if self._set_mode_timeout > 0:
|
|
||||||
self._set_mode_timeout -= 1
|
|
||||||
|
|
||||||
def update(self, sm: messaging.SubMaster) -> None:
|
def update(self, sm: messaging.SubMaster) -> None:
|
||||||
self._read_params()
|
self._read_params()
|
||||||
@@ -385,6 +383,6 @@ class DynamicExperimentalController:
|
|||||||
else:
|
else:
|
||||||
self._radar_mode()
|
self._radar_mode()
|
||||||
|
|
||||||
|
self._mode_manager.update()
|
||||||
self._active = sm['selfdriveState'].experimentalMode and self._enabled
|
self._active = sm['selfdriveState'].experimentalMode and self._enabled
|
||||||
|
|
||||||
self._frame += 1
|
self._frame += 1
|
||||||
|
|||||||
@@ -1,248 +1,94 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import numpy as np
|
|
||||||
from openpilot.common.params import Params
|
|
||||||
|
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants, SNG_State
|
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController, TRAJECTORY_SIZE, STOP_AND_GO_FRAME
|
|
||||||
|
|
||||||
class MockInterp:
|
|
||||||
def __call__(self, x, xp, fp):
|
|
||||||
return np.interp(x, xp, fp)
|
|
||||||
|
|
||||||
class MockCarState:
|
|
||||||
def __init__(self, v_ego=0., standstill=False, left_blinker=False, right_blinker=False):
|
|
||||||
self.vEgo = v_ego
|
|
||||||
self.standstill = standstill
|
|
||||||
self.leftBlinker = left_blinker
|
|
||||||
self.rightBlinker = right_blinker
|
|
||||||
|
|
||||||
class MockLeadOne:
|
class MockLeadOne:
|
||||||
def __init__(self, status=False, d_rel=0):
|
def __init__(self, status=0.0):
|
||||||
self.status = status
|
self.status = status
|
||||||
self.dRel = d_rel
|
|
||||||
|
class MockRadarState:
|
||||||
|
def __init__(self, status=0.0):
|
||||||
|
self.leadOne = MockLeadOne(status=status)
|
||||||
|
|
||||||
|
class MockCarState:
|
||||||
|
def __init__(self, vEgo=0.0, vCruise=0.0, standstill=False):
|
||||||
|
self.vEgo = vEgo
|
||||||
|
self.vCruise = vCruise
|
||||||
|
self.standstill = standstill
|
||||||
|
|
||||||
class MockModelData:
|
class MockModelData:
|
||||||
def __init__(self, x_vals=None, positions=None):
|
def __init__(self, valid=True):
|
||||||
self.orientation = type('Orientation', (), {'x': x_vals})()
|
size = 33 if valid else 10 # incomplete if invalid
|
||||||
self.position = type('Position', (), {'x': positions})()
|
self.position = type("Pos", (), {"x": [0.0] * size})()
|
||||||
|
self.orientation = type("Ori", (), {"x": [0.0] * size})()
|
||||||
|
|
||||||
class MockControlState:
|
class MockSelfDriveState:
|
||||||
def __init__(self, v_cruise=0):
|
def __init__(self, experimentalMode=False):
|
||||||
self.vCruise = v_cruise
|
self.experimentalMode = experimentalMode
|
||||||
|
|
||||||
|
class MockParams:
|
||||||
|
def get_bool(self, name):
|
||||||
|
return True
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def interp(monkeypatch):
|
def default_sm():
|
||||||
mock_interp = MockInterp()
|
sm = {
|
||||||
monkeypatch.setattr('numpy.interp', mock_interp)
|
'carState': MockCarState(vEgo=10.0, vCruise=20.0),
|
||||||
return mock_interp
|
'radarState': MockRadarState(status=1.0),
|
||||||
|
'modelV2': MockModelData(valid=True),
|
||||||
|
'selfdriveState': MockSelfDriveState(experimentalMode=True),
|
||||||
|
}
|
||||||
|
return sm
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def controller(interp):
|
def mock_cp():
|
||||||
params = Params()
|
class CP:
|
||||||
params.put_bool("DynamicExperimentalControl", True)
|
radarUnavailable = False
|
||||||
return DynamicExperimentalController()
|
return CP()
|
||||||
|
|
||||||
def test_initial_state(controller):
|
@pytest.fixture
|
||||||
"""Test initial state of the controller"""
|
def mock_mpc():
|
||||||
assert controller._mode == 'acc'
|
class MPC:
|
||||||
assert not controller._has_lead
|
crash_cnt = 0
|
||||||
assert not controller._has_standstill
|
return MPC()
|
||||||
assert controller._sng_state == SNG_State.off
|
|
||||||
assert not controller._has_lead_filtered
|
|
||||||
assert not controller._has_slow_down
|
|
||||||
assert not controller._has_dangerous_ttc
|
|
||||||
assert not controller._has_mpc_fcw
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"])
|
# Fake Kalman Filter that always returns a given value
|
||||||
def test_standstill_detection(controller, has_radar):
|
class FakeKalman:
|
||||||
"""Test standstill detection and state transitions"""
|
def __init__(self, value=1.0):
|
||||||
car_state = MockCarState(standstill=True)
|
self.value = value
|
||||||
lead_one = MockLeadOne()
|
def add_data(self, v): pass
|
||||||
md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE)
|
def get_value(self): return self.value
|
||||||
controls_state = MockControlState()
|
def get_confidence(self): return 1.0
|
||||||
|
def reset_data(self): pass
|
||||||
|
|
||||||
# Test transition to standstill
|
def test_initial_mode_is_acc(mock_cp, mock_mpc):
|
||||||
controller.update(not has_radar, car_state, lead_one, md, controls_state)
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
assert controller._sng_state == SNG_State.stopped
|
assert controller.mode() == "acc"
|
||||||
assert controller.get_mpc_mode() == 'blended'
|
|
||||||
|
|
||||||
# Test transition from standstill to moving
|
def test_standstill_triggers_blended(mock_cp, mock_mpc, default_sm):
|
||||||
car_state.standstill = False
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
controller.update(not has_radar, car_state, lead_one, md, controls_state)
|
default_sm['carState'].standstill = True
|
||||||
assert controller._sng_state == SNG_State.going
|
for _ in range(10):
|
||||||
|
controller.update(default_sm)
|
||||||
|
assert controller.mode() == "blended"
|
||||||
|
|
||||||
# Test complete transition to normal driving
|
def test_emergency_blended_on_fcw(mock_cp, mock_mpc, default_sm):
|
||||||
for _ in range(STOP_AND_GO_FRAME + 1):
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
controller.update(not has_radar, car_state, lead_one, md, controls_state)
|
mock_mpc.crash_cnt = 1 # simulate FCW
|
||||||
assert controller._sng_state == SNG_State.off
|
for _ in range(2):
|
||||||
|
controller.update(default_sm)
|
||||||
|
assert controller.mode() == "blended"
|
||||||
|
|
||||||
@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"])
|
def test_radarless_slowdown_triggers_blended(mock_cp, mock_mpc, default_sm):
|
||||||
def test_lead_detection(controller, has_radar):
|
mock_cp.radarUnavailable = True
|
||||||
"""Test lead vehicle detection and filtering"""
|
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||||
car_state = MockCarState(v_ego=20) # 72 kph
|
|
||||||
lead_one = MockLeadOne(status=True, d_rel=50) # Safe distance
|
|
||||||
md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE)
|
|
||||||
controls_state = MockControlState(v_cruise=72)
|
|
||||||
|
|
||||||
# Let moving average stabilize
|
# Force conditions to simulate slowdown
|
||||||
for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1):
|
controller._slow_down_filter = FakeKalman(value=1.0) # Ensure urgency triggers slowdown
|
||||||
controller.update(not has_radar, car_state, lead_one, md, controls_state)
|
controller._v_ego_kph = 35.0
|
||||||
|
default_sm['modelV2'] = MockModelData(valid=False) # Incomplete trajectory
|
||||||
|
|
||||||
assert controller._has_lead_filtered
|
for _ in range(3):
|
||||||
expected_mode = 'acc' if has_radar else 'blended'
|
controller.update(default_sm)
|
||||||
assert controller.get_mpc_mode() == expected_mode
|
|
||||||
|
|
||||||
# Test lead loss detection
|
assert controller.mode() == "blended"
|
||||||
lead_one.status = False
|
|
||||||
for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1):
|
|
||||||
controller.update(not has_radar, car_state, lead_one, md, controls_state)
|
|
||||||
|
|
||||||
assert not controller._has_lead_filtered
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"])
|
|
||||||
def test_slow_down_detection(controller, has_radar):
|
|
||||||
"""Test slow down detection based on trajectory"""
|
|
||||||
car_state = MockCarState(v_ego=10/3.6) # 10 kph
|
|
||||||
lead_one = MockLeadOne()
|
|
||||||
x_vals = [0] * TRAJECTORY_SIZE
|
|
||||||
positions = [20] * TRAJECTORY_SIZE # Position within slow down threshold
|
|
||||||
md = MockModelData(x_vals=x_vals, positions=positions)
|
|
||||||
controls_state = MockControlState(v_cruise=30)
|
|
||||||
|
|
||||||
# Test slow down detection
|
|
||||||
for _ in range(WMACConstants.SLOW_DOWN_WINDOW_SIZE + 1):
|
|
||||||
controller.update(not has_radar, car_state, lead_one, md, controls_state)
|
|
||||||
|
|
||||||
assert controller._has_slow_down
|
|
||||||
assert controller.get_mpc_mode() == 'blended'
|
|
||||||
|
|
||||||
# Test slow down recovery
|
|
||||||
positions = [200] * TRAJECTORY_SIZE # Position outside slow down threshold
|
|
||||||
md = MockModelData(x_vals=x_vals, positions=positions)
|
|
||||||
for _ in range(WMACConstants.SLOW_DOWN_WINDOW_SIZE + 1):
|
|
||||||
controller.update(not has_radar, car_state, lead_one, md, controls_state)
|
|
||||||
|
|
||||||
assert not controller._has_slow_down
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"])
|
|
||||||
def test_dangerous_ttc_detection(controller, has_radar):
|
|
||||||
"""Test Time-To-Collision detection and handling"""
|
|
||||||
car_state = MockCarState(v_ego=10) # 36 kph
|
|
||||||
lead_one = MockLeadOne(status=True)
|
|
||||||
md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE)
|
|
||||||
controls_state = MockControlState(v_cruise=36)
|
|
||||||
|
|
||||||
# First establish normal conditions with lead
|
|
||||||
lead_one.dRel = 100 # Safe distance
|
|
||||||
for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): # First establish lead detection
|
|
||||||
controller.update(not has_radar, car_state, lead_one, md, controls_state)
|
|
||||||
|
|
||||||
assert controller._has_lead_filtered # Verify lead is detected
|
|
||||||
|
|
||||||
# Now test dangerous TTC detection
|
|
||||||
lead_one.dRel = 10 # 10m distance - should trigger dangerous TTC
|
|
||||||
# TTC = dRel/vEgo = 10/10 = 1s (which is less than DANGEROUS_TTC = 2.3s)
|
|
||||||
|
|
||||||
# Need to update multiple times to allow the weighted average to stabilize
|
|
||||||
for _ in range(WMACConstants.DANGEROUS_TTC_WINDOW_SIZE * 2):
|
|
||||||
controller.update(not has_radar, car_state, lead_one, md, controls_state)
|
|
||||||
|
|
||||||
assert controller._has_dangerous_ttc, "TTC of 1s should be considered dangerous"
|
|
||||||
expected_mode = 'acc' if has_radar else 'blended'
|
|
||||||
assert controller.get_mpc_mode() == expected_mode, f"Should be in [{expected_mode}] mode with dangerous TTC"
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"])
|
|
||||||
def test_mode_transitions(controller, has_radar):
|
|
||||||
"""Test comprehensive mode transitions under different conditions"""
|
|
||||||
# Initialize with normal driving conditions
|
|
||||||
car_state = MockCarState(v_ego=25) # 90 kph
|
|
||||||
lead_one = MockLeadOne(status=False)
|
|
||||||
md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[200] * TRAJECTORY_SIZE)
|
|
||||||
controls_state = MockControlState(v_cruise=100)
|
|
||||||
|
|
||||||
def stabilize_filters():
|
|
||||||
"""Helper to let all moving averages stabilize"""
|
|
||||||
for _ in range(max(WMACConstants.LEAD_WINDOW_SIZE, WMACConstants.SLOW_DOWN_WINDOW_SIZE,
|
|
||||||
WMACConstants.DANGEROUS_TTC_WINDOW_SIZE, WMACConstants.MPC_FCW_WINDOW_SIZE) + 1):
|
|
||||||
controller.update(not has_radar, car_state, lead_one, md, controls_state)
|
|
||||||
|
|
||||||
# Test 1: Normal driving -> ACC mode
|
|
||||||
stabilize_filters()
|
|
||||||
assert controller.get_mpc_mode() == 'acc', "Should be in ACC mode under normal driving conditions"
|
|
||||||
|
|
||||||
# Test 2: Standstill -> Blended mode
|
|
||||||
car_state.standstill = True
|
|
||||||
controller.update(not has_radar, car_state, lead_one, md, controls_state)
|
|
||||||
assert controller.get_mpc_mode() == 'blended', "Should be in blended mode during standstill"
|
|
||||||
|
|
||||||
# Test 3: Lead car appears -> ACC mode
|
|
||||||
car_state = MockCarState(v_ego=20) # Reset car state
|
|
||||||
lead_one.status = True
|
|
||||||
lead_one.dRel = 50 # Safe distance
|
|
||||||
stabilize_filters()
|
|
||||||
assert not controller._has_dangerous_ttc, "Should not have dangerous TTC"
|
|
||||||
assert controller.get_mpc_mode() == 'acc', "Should be in ACC mode with safe lead distance"
|
|
||||||
|
|
||||||
# Test 4: Dangerous TTC -> Blended mode
|
|
||||||
car_state = MockCarState(v_ego=20) # 72 kph
|
|
||||||
lead_one.status = True
|
|
||||||
lead_one.dRel = 50 # First establish normal lead detection
|
|
||||||
|
|
||||||
# First establish lead detection
|
|
||||||
for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1):
|
|
||||||
controller.update(not has_radar, car_state, lead_one, md, controls_state)
|
|
||||||
|
|
||||||
assert controller._has_lead_filtered # Verify lead is detected
|
|
||||||
|
|
||||||
# Now create dangerous TTC condition
|
|
||||||
lead_one.dRel = 20 # This creates a TTC of 1s, well below DANGEROUS_TTC
|
|
||||||
|
|
||||||
for _ in range(WMACConstants.DANGEROUS_TTC_WINDOW_SIZE * 2):
|
|
||||||
controller.update(not has_radar, car_state, lead_one, md, controls_state)
|
|
||||||
|
|
||||||
assert controller._has_dangerous_ttc, "Should detect dangerous TTC condition"
|
|
||||||
expected_mode = 'acc' if has_radar else 'blended'
|
|
||||||
assert controller.get_mpc_mode() == expected_mode, f"Should be in [{expected_mode}] mode with dangerous TTC"
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"])
|
|
||||||
def test_mpc_fcw_handling(controller, has_radar):
|
|
||||||
"""Test MPC FCW crash count handling and mode transitions"""
|
|
||||||
car_state = MockCarState(v_ego=20)
|
|
||||||
lead_one = MockLeadOne()
|
|
||||||
md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE)
|
|
||||||
controls_state = MockControlState(v_cruise=72)
|
|
||||||
|
|
||||||
# Test FCW activation
|
|
||||||
controller.set_mpc_fcw_crash_cnt(5)
|
|
||||||
for _ in range(WMACConstants.MPC_FCW_WINDOW_SIZE + 1):
|
|
||||||
controller.update(not has_radar, car_state, lead_one, md, controls_state)
|
|
||||||
|
|
||||||
assert controller._has_mpc_fcw
|
|
||||||
assert controller.get_mpc_mode() == 'blended'
|
|
||||||
|
|
||||||
# Test FCW recovery
|
|
||||||
controller.set_mpc_fcw_crash_cnt(0)
|
|
||||||
for _ in range(WMACConstants.MPC_FCW_WINDOW_SIZE + 1):
|
|
||||||
controller.update(not has_radar, car_state, lead_one, md, controls_state)
|
|
||||||
|
|
||||||
assert not controller._has_mpc_fcw
|
|
||||||
|
|
||||||
def test_radar_unavailable_handling(controller):
|
|
||||||
"""Test behavior transitions between radar available and unavailable states"""
|
|
||||||
car_state = MockCarState(v_ego=27.78) # 100 kph
|
|
||||||
lead_one = MockLeadOne(status=True, d_rel=50)
|
|
||||||
md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE)
|
|
||||||
controls_state = MockControlState(v_cruise=100)
|
|
||||||
|
|
||||||
# Test with radar available
|
|
||||||
for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1):
|
|
||||||
controller.update(False, car_state, lead_one, md, controls_state)
|
|
||||||
radar_mode = controller.get_mpc_mode()
|
|
||||||
|
|
||||||
# Test with radar unavailable
|
|
||||||
for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1):
|
|
||||||
controller.update(True, car_state, lead_one, md, controls_state)
|
|
||||||
radarless_mode = controller.get_mpc_mode()
|
|
||||||
|
|
||||||
assert radar_mode is not None
|
|
||||||
assert radarless_mode is not None
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import numpy as np
|
|||||||
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
|
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
|
||||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||||
|
|
||||||
|
from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle
|
||||||
|
|
||||||
LAT_PLAN_MIN_IDX = 5
|
LAT_PLAN_MIN_IDX = 5
|
||||||
LATERAL_LAG_MOD = 0.0 # seconds, modifies how far in the future we look ahead for the lateral plan
|
LATERAL_LAG_MOD = 0.0 # seconds, modifies how far in the future we look ahead for the lateral plan
|
||||||
|
|
||||||
@@ -41,8 +43,9 @@ def get_lookahead_value(future_vals, current_val):
|
|||||||
return min_val
|
return min_val
|
||||||
|
|
||||||
|
|
||||||
class LatControlTorqueExtBase:
|
class LatControlTorqueExtBase(LagdToggle):
|
||||||
def __init__(self, lac_torque, CP, CP_SP):
|
def __init__(self, lac_torque, CP, CP_SP):
|
||||||
|
LagdToggle.__init__(self)
|
||||||
self.model_v2 = None
|
self.model_v2 = None
|
||||||
self.model_valid = False
|
self.model_valid = False
|
||||||
self.use_steering_angle = lac_torque.use_steering_angle
|
self.use_steering_angle = lac_torque.use_steering_angle
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ See the LICENSE.md file in the root directory for more details.
|
|||||||
from cereal import messaging, custom
|
from cereal import messaging, custom
|
||||||
from opendbc.car import structs
|
from opendbc.car import structs
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController
|
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController
|
||||||
|
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||||
|
|
||||||
DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState
|
DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState
|
||||||
|
|
||||||
@@ -15,6 +16,12 @@ DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimen
|
|||||||
class LongitudinalPlannerSP:
|
class LongitudinalPlannerSP:
|
||||||
def __init__(self, CP: structs.CarParams, mpc):
|
def __init__(self, CP: structs.CarParams, mpc):
|
||||||
self.dec = DynamicExperimentalController(CP, mpc)
|
self.dec = DynamicExperimentalController(CP, mpc)
|
||||||
|
model_bundle = get_active_bundle()
|
||||||
|
self.generation = model_bundle.generation if model_bundle is not None else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mlsim(self) -> bool:
|
||||||
|
return self.generation == 11
|
||||||
|
|
||||||
def get_mpc_mode(self) -> str | None:
|
def get_mpc_mode(self) -> str | None:
|
||||||
if not self.dec.active():
|
if not self.dec.active():
|
||||||
|
|||||||
@@ -22,14 +22,16 @@ class ParamStore:
|
|||||||
|
|
||||||
self.keys = universal_params + brand_params
|
self.keys = universal_params + brand_params
|
||||||
self.values = {}
|
self.values = {}
|
||||||
|
self.cached_params_list: list[capnp.lib.capnp._DynamicStructBuilder] | None = None
|
||||||
|
|
||||||
def update(self, params: Params) -> None:
|
def update(self, params: Params) -> None:
|
||||||
|
old_values = dict(self.values)
|
||||||
self.values = {k: params.get(k, encoding='utf8') or "0" for k in self.keys}
|
self.values = {k: params.get(k, encoding='utf8') or "0" for k in self.keys}
|
||||||
|
if old_values != self.values:
|
||||||
|
self.cached_params_list = None
|
||||||
|
|
||||||
def publish(self) -> list[capnp.lib.capnp._DynamicStructBuilder]:
|
def publish(self) -> list[capnp.lib.capnp._DynamicStructBuilder]:
|
||||||
params_list: list[capnp.lib.capnp._DynamicStructBuilder] = []
|
if self.cached_params_list is None:
|
||||||
|
# TODO-SP: Why are we doing a list instead of a dictionary here?
|
||||||
for k in self.keys:
|
self.cached_params_list = [custom.CarControlSP.Param(key=k, value=self.values[k]) for k in self.keys]
|
||||||
params_list.append(custom.CarControlSP.Param(key=k, value=self.values[k]))
|
return self.cached_params_list
|
||||||
|
|
||||||
return params_list
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from pathlib import Path
|
|||||||
from datetime import datetime, timedelta, UTC
|
from datetime import datetime, timedelta, UTC
|
||||||
from openpilot.common.api import api_get
|
from openpilot.common.api import api_get
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.common.spinner import Spinner
|
||||||
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
||||||
from openpilot.system.hardware import HARDWARE, PC
|
from openpilot.system.hardware import HARDWARE, PC
|
||||||
from openpilot.system.hardware.hw import Paths
|
from openpilot.system.hardware.hw import Paths
|
||||||
@@ -44,7 +45,6 @@ def register(show_spinner=False) -> str | None:
|
|||||||
cloudlog.warning(f"missing public key: {pubkey}")
|
cloudlog.warning(f"missing public key: {pubkey}")
|
||||||
elif dongle_id is None:
|
elif dongle_id is None:
|
||||||
if show_spinner:
|
if show_spinner:
|
||||||
from openpilot.system.ui.spinner import Spinner
|
|
||||||
spinner = Spinner()
|
spinner = Spinner()
|
||||||
spinner.update("registering device")
|
spinner.update("registering device")
|
||||||
|
|
||||||
|
|||||||
@@ -56,28 +56,28 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "boot",
|
"name": "boot",
|
||||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-4143170bad94968fd9be870b1498b4100bf273ed0aec2a2601c9017991d4bd42.img.xz",
|
"url": "https://commadist.azureedge.net/agnosupdate/boot-4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef.img.xz",
|
||||||
"hash": "4143170bad94968fd9be870b1498b4100bf273ed0aec2a2601c9017991d4bd42",
|
"hash": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef",
|
||||||
"hash_raw": "4143170bad94968fd9be870b1498b4100bf273ed0aec2a2601c9017991d4bd42",
|
"hash_raw": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef",
|
||||||
"size": 18479104,
|
"size": 18479104,
|
||||||
"sparse": false,
|
"sparse": false,
|
||||||
"full_check": true,
|
"full_check": true,
|
||||||
"has_ab": true,
|
"has_ab": true,
|
||||||
"ondevice_hash": "6b7b3371100ad36d8a5a9ff19a1663b9b9e2d5e99cbe3cf9255e9c3017291ce3"
|
"ondevice_hash": "8d7094d774faa4e801e36b403a31b53b913b31d086f4dc682d2f64710c557e8a"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "system",
|
"name": "system",
|
||||||
"url": "https://commadist.azureedge.net/agnosupdate/system-c51bb5841011728f7cf108a9138ba68228ffb4232dfd91d6e082a6d8a6a8deaa.img.xz",
|
"url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img.xz",
|
||||||
"hash": "993d6a1cd2b684e2b1cf6ff840f8996f02a529011372d9c1471e4c80719e7da9",
|
"hash": "cccd7073d067027396f2afd49874729757db0bbbc79853a0bf2938bd356fe164",
|
||||||
"hash_raw": "c51bb5841011728f7cf108a9138ba68228ffb4232dfd91d6e082a6d8a6a8deaa",
|
"hash_raw": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39",
|
||||||
"size": 5368709120,
|
"size": 5368709120,
|
||||||
"sparse": true,
|
"sparse": true,
|
||||||
"full_check": false,
|
"full_check": false,
|
||||||
"has_ab": true,
|
"has_ab": true,
|
||||||
"ondevice_hash": "59db25651da977eeb16a1af741fd01fc3d6b50d21544b1a7428b7c86b2cdef2d",
|
"ondevice_hash": "c7707f16ce7d977748677cc354e250943b4ff6c21b9a19a492053d32397cf9ec",
|
||||||
"alt": {
|
"alt": {
|
||||||
"hash": "c51bb5841011728f7cf108a9138ba68228ffb4232dfd91d6e082a6d8a6a8deaa",
|
"hash": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39",
|
||||||
"url": "https://commadist.azureedge.net/agnosupdate/system-c51bb5841011728f7cf108a9138ba68228ffb4232dfd91d6e082a6d8a6a8deaa.img",
|
"url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img",
|
||||||
"size": 5368709120
|
"size": 5368709120
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user