mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-02-18 20:03:53 +08:00
Merge branch 'upstream/openpilot/master' into 0508-sync
This commit is contained in:
2
.github/labeler.yaml
vendored
2
.github/labeler.yaml
vendored
@@ -16,7 +16,7 @@ simulation:
|
||||
|
||||
ui:
|
||||
- changed-files:
|
||||
- any-glob-to-all-files: 'selfdrive/ui/**'
|
||||
- any-glob-to-all-files: '{selfdrive/ui/**,system/ui/**}'
|
||||
|
||||
tools:
|
||||
- changed-files:
|
||||
|
||||
2
.github/workflows/auto-cache/action.yaml
vendored
2
.github/workflows/auto-cache/action.yaml
vendored
@@ -12,7 +12,7 @@ inputs:
|
||||
required: true
|
||||
save:
|
||||
description: 'whether to save the cache'
|
||||
default: 'false'
|
||||
default: 'true'
|
||||
required: false
|
||||
outputs:
|
||||
cache-hit:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -47,10 +47,8 @@ selfdrive/pandad/pandad
|
||||
cereal/services.h
|
||||
cereal/gen
|
||||
cereal/messaging/bridge
|
||||
selfdrive/logcatd/logcatd
|
||||
selfdrive/mapd/default_speeds_by_region.json
|
||||
system/proclogd/proclogd
|
||||
selfdrive/ui/translations/alerts_generated.h
|
||||
selfdrive/ui/translations/tmp
|
||||
selfdrive/test/longitudinal_maneuvers/out
|
||||
selfdrive/car/tests/cars_dump
|
||||
|
||||
@@ -4,6 +4,7 @@ Version 0.9.9 (2025-05-15)
|
||||
* New training architecture supervised by MLSIM
|
||||
* Steering actuator delay is now learned online
|
||||
* Tesla Model 3 and Y support thanks to lukasloetkolben!
|
||||
* Lexus RC 2023 support thanks to nelsonjchen!
|
||||
* Coming soon
|
||||
* New Honda models
|
||||
* Bigger vision model
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define DEFAULT_MODEL "Filet o Fish (Default)"
|
||||
#define DEFAULT_MODEL "Tomb Raider 7 (Default)"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Utilities for reading real time clocks and keeping soft real time constraints."""
|
||||
import gc
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from setproctitle import getproctitle
|
||||
@@ -28,13 +29,13 @@ class Priority:
|
||||
|
||||
|
||||
def set_core_affinity(cores: list[int]) -> None:
|
||||
if not PC:
|
||||
if sys.platform == 'linux' and not PC:
|
||||
os.sched_setaffinity(0, cores)
|
||||
|
||||
|
||||
def config_realtime_process(cores: int | list[int], priority: int) -> None:
|
||||
gc.disable()
|
||||
if not PC:
|
||||
if sys.platform == 'linux' and not PC:
|
||||
os.sched_setscheduler(0, os.SCHED_FIFO, os.sched_param(priority))
|
||||
c = cores if isinstance(cores, list) else [cores, ]
|
||||
set_core_affinity(c)
|
||||
|
||||
19
docs/CARS.md
19
docs/CARS.md
@@ -4,12 +4,13 @@
|
||||
|
||||
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
|
||||
|
||||
# 304 Supported Cars
|
||||
# 311 Supported Cars
|
||||
|
||||
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br> |Video|
|
||||
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
|Acura|ILX 2016-19|AcuraWatch Plus|openpilot|26 mph|25 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=Acura&model=ILX 2016-19">Buy Here</a></sub></details>||
|
||||
|Acura|RDX 2016-18|AcuraWatch Plus|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=Acura&model=RDX 2016-18">Buy Here</a></sub></details>||
|
||||
|Acura|ILX 2016-18|Technology Plus Package or AcuraWatch Plus|openpilot|26 mph|25 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=Acura&model=ILX 2016-18">Buy Here</a></sub></details>||
|
||||
|Acura|ILX 2019|All|openpilot|26 mph|25 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=Acura&model=ILX 2019">Buy Here</a></sub></details>||
|
||||
|Acura|RDX 2016-18|AcuraWatch Plus or Advance Package|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=Acura&model=RDX 2016-18">Buy Here</a></sub></details>||
|
||||
|Acura|RDX 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 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=Acura&model=RDX 2019-21">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>||
|
||||
@@ -32,17 +33,22 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA 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=Dodge&model=Durango 2020-21">Buy Here</a></sub></details>||
|
||||
|Ford|Bronco Sport 2021-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=Bronco Sport 2021-24">Buy Here</a></sub></details>||
|
||||
|Ford|Escape 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 2020-22">Buy Here</a></sub></details>||
|
||||
|Ford|Escape 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 2023-24">Buy Here</a></sub></details>||
|
||||
|Ford|Escape 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 Hybrid 2020-22">Buy Here</a></sub></details>||
|
||||
|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>||
|
||||
|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>||
|
||||
|Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Explorer 2020-24">Buy Here</a></sub></details>||
|
||||
|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Explorer Hybrid 2020-24">Buy Here</a></sub></details>||
|
||||
|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 USB-C coupler<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=F-150 2021-23">Buy Here</a></sub></details>||
|
||||
|Ford|F-150 Hybrid 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 Hybrid 2021-23">Buy Here</a></sub></details>||
|
||||
|Ford|Focus 2018[<sup>3</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|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=Focus 2018">Buy Here</a></sub></details>||
|
||||
|Ford|Focus Hybrid 2018[<sup>3</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|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=Focus Hybrid 2018">Buy Here</a></sub></details>||
|
||||
|Ford|Kuga 2020-22|Adaptive Cruise Control with Lane Centering|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=Kuga 2020-22">Buy Here</a></sub></details>||
|
||||
|Ford|Kuga Hybrid 2020-22|Adaptive Cruise Control with Lane Centering|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=Kuga Hybrid 2020-22">Buy Here</a></sub></details>||
|
||||
|Ford|Kuga Plug-in Hybrid 2020-22|Adaptive Cruise Control with Lane Centering|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=Kuga Plug-in Hybrid 2020-22">Buy Here</a></sub></details>||
|
||||
|Ford|Kuga 2020-23|Adaptive Cruise Control with Lane Centering|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=Kuga 2020-23">Buy Here</a></sub></details>||
|
||||
|Ford|Kuga Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|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=Kuga Hybrid 2020-23">Buy Here</a></sub></details>||
|
||||
|Ford|Kuga Hybrid 2024|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=Kuga Hybrid 2024">Buy Here</a></sub></details>||
|
||||
|Ford|Kuga Plug-in Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|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=Kuga Plug-in Hybrid 2020-23">Buy Here</a></sub></details>||
|
||||
|Ford|Kuga Plug-in Hybrid 2024|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=Kuga Plug-in Hybrid 2024">Buy Here</a></sub></details>||
|
||||
|Ford|Maverick 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 2022">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>||
|
||||
@@ -186,6 +192,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Lexus|NX Hybrid 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 Hybrid 2018-19">Buy Here</a></sub></details>||
|
||||
|Lexus|NX Hybrid 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 Hybrid 2020-21">Buy Here</a></sub></details>||
|
||||
|Lexus|RC 2018-20|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=RC 2018-20">Buy Here</a></sub></details>||
|
||||
|Lexus|RC 2023|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=RC 2023">Buy Here</a></sub></details>||
|
||||
|Lexus|RX 2016|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=RX 2016">Buy Here</a></sub></details>||
|
||||
|Lexus|RX 2017-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=RX 2017-19">Buy Here</a></sub></details>||
|
||||
|Lexus|RX 2020-22|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=RX 2020-22">Buy Here</a></sub></details>||
|
||||
|
||||
@@ -25,9 +25,9 @@ ensuring two main safety requirements.
|
||||
by stepping on the brake pedal or by pressing the cancel button.
|
||||
2. The vehicle must not alter its trajectory too quickly for the driver to safely
|
||||
react. This means that while the system is engaged, the actuators are constrained
|
||||
to operate within reasonable limits[^1].
|
||||
to operate within reasonable limits[^1].
|
||||
|
||||
For additional safety implementation details, refer to [panda safety model](https://github.com/commaai/panda#safety-model). For vehicle specific implementation of the safety concept, refer to [panda/board/safety/](https://github.com/commaai/panda/tree/master/board/safety).
|
||||
For additional safety implementation details, refer to [panda safety model](https://github.com/commaai/panda#safety-model). For vehicle specific implementation of the safety concept, refer to [opendbc/safety/safety](https://github.com/commaai/opendbc/tree/master/opendbc/safety/safety).
|
||||
|
||||
**Extra note**: comma.ai strongly discourages the use of openpilot forks with safety code either missing or
|
||||
not fully meeting the above requirements.
|
||||
|
||||
@@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1
|
||||
export VECLIB_MAXIMUM_THREADS=1
|
||||
|
||||
if [ -z "$AGNOS_VERSION" ]; then
|
||||
export AGNOS_VERSION="11.13"
|
||||
export AGNOS_VERSION="12.1"
|
||||
fi
|
||||
|
||||
export STAGING_ROOT="/data/safe_staging"
|
||||
|
||||
Submodule opendbc_repo updated: ab8640fde2...029bc6abc4
@@ -47,6 +47,7 @@ dependencies = [
|
||||
# logging
|
||||
"pyzmq",
|
||||
"sentry-sdk",
|
||||
"xattr", # used in place of 'os.getxattr' for macos compatibility
|
||||
|
||||
# athena
|
||||
"PyJWT",
|
||||
|
||||
@@ -191,7 +191,7 @@ class CarSpecificEvents:
|
||||
events.add(EventName.accFaulted)
|
||||
if CS.steeringPressed:
|
||||
events.add(EventName.steerOverride)
|
||||
if CS.steeringDisengage:
|
||||
if CS.steeringDisengage and not CS_prev.steeringDisengage:
|
||||
events.add(EventName.steerDisengage)
|
||||
if CS.brakePressed and CS.standstill:
|
||||
events.add(EventName.preEnableStandstill)
|
||||
|
||||
@@ -4,7 +4,7 @@ from openpilot.common.basedir import BASEDIR
|
||||
from opendbc.car.docs import generate_cars_md, get_all_car_docs
|
||||
from openpilot.selfdrive.debug.dump_car_docs import dump_car_docs
|
||||
from openpilot.selfdrive.debug.print_docs_diff import print_car_docs_diff
|
||||
from openpilot.selfdrive.car.docs import CARS_MD_OUT, CARS_MD_TEMPLATE
|
||||
from openpilot.selfdrive.car.docs import CARS_MD_TEMPLATE
|
||||
|
||||
|
||||
class TestCarDocs:
|
||||
@@ -13,11 +13,7 @@ class TestCarDocs:
|
||||
cls.all_cars = get_all_car_docs()
|
||||
|
||||
def test_generator(self):
|
||||
generated_cars_md = generate_cars_md(self.all_cars, CARS_MD_TEMPLATE)
|
||||
with open(CARS_MD_OUT) as f:
|
||||
current_cars_md = f.read()
|
||||
|
||||
assert generated_cars_md == current_cars_md, "Run selfdrive/car/docs.py to update the compatibility documentation"
|
||||
generate_cars_md(self.all_cars, CARS_MD_TEMPLATE)
|
||||
|
||||
def test_docs_diff(self):
|
||||
dump_path = os.path.join(BASEDIR, "selfdrive", "car", "tests", "cars_dump")
|
||||
|
||||
@@ -93,10 +93,10 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
return x, v, a, j, throttle_prob
|
||||
|
||||
def update(self, sm):
|
||||
self.mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc'
|
||||
LongitudinalPlannerSP.update(self, sm)
|
||||
self.mpc.mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc'
|
||||
if dec_mpc_mode := self.get_mpc_mode():
|
||||
self.mpc.mode = dec_mpc_mode
|
||||
self.mode = dec_mpc_mode
|
||||
|
||||
if len(sm['carControl'].orientationNED) == 3:
|
||||
accel_coast = get_coast_accel(sm['carControl'].orientationNED[1])
|
||||
@@ -119,7 +119,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
# No change cost when user is controlling the speed, or when standstill
|
||||
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
|
||||
|
||||
if self.mpc.mode == 'acc':
|
||||
if self.mode == 'acc':
|
||||
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
|
||||
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
|
||||
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
|
||||
@@ -166,8 +166,17 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
self.v_desired_filter.x = self.v_desired_filter.x + self.dt * (self.a_desired + a_prev) / 2.0
|
||||
|
||||
action_t = self.CP.longitudinalActuatorDelay + DT_MDL
|
||||
output_a_target, self.output_should_stop = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, CONTROL_N_T_IDX,
|
||||
output_a_target_mpc, output_should_stop_mpc = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, CONTROL_N_T_IDX,
|
||||
action_t=action_t, vEgoStopping=self.CP.vEgoStopping)
|
||||
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
|
||||
output_should_stop_e2e = sm['modelV2'].action.shouldStop
|
||||
|
||||
if self.mode == 'acc':
|
||||
output_a_target = output_a_target_mpc
|
||||
self.output_should_stop = output_should_stop_mpc
|
||||
else:
|
||||
output_a_target = min(output_a_target_mpc, output_a_target_e2e)
|
||||
self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc
|
||||
|
||||
for idx in range(2):
|
||||
accel_clip[idx] = np.clip(accel_clip[idx], self.prev_accel_clip[idx] - 0.05, self.prev_accel_clip[idx] + 0.05)
|
||||
|
||||
@@ -90,17 +90,11 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D
|
||||
fill_xyzt(modelV2.orientationRate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T)
|
||||
|
||||
# temporal pose
|
||||
temporal_pose = modelV2.temporalPose
|
||||
if 'sim_pose' in net_output_data:
|
||||
temporal_pose.trans = net_output_data['sim_pose'][0,:ModelConstants.POSE_WIDTH//2].tolist()
|
||||
temporal_pose.transStd = net_output_data['sim_pose_stds'][0,:ModelConstants.POSE_WIDTH//2].tolist()
|
||||
temporal_pose.rot = net_output_data['sim_pose'][0,ModelConstants.POSE_WIDTH//2:].tolist()
|
||||
temporal_pose.rotStd = net_output_data['sim_pose_stds'][0,ModelConstants.POSE_WIDTH//2:].tolist()
|
||||
else:
|
||||
temporal_pose.trans = net_output_data['plan'][0,0,Plan.VELOCITY].tolist()
|
||||
temporal_pose.transStd = net_output_data['plan_stds'][0,0,Plan.VELOCITY].tolist()
|
||||
temporal_pose.rot = net_output_data['plan'][0,0,Plan.ORIENTATION_RATE].tolist()
|
||||
temporal_pose.rotStd = net_output_data['plan_stds'][0,0,Plan.ORIENTATION_RATE].tolist()
|
||||
#temporal_pose = modelV2.temporalPose
|
||||
#temporal_pose.trans = net_output_data['sim_pose'][0,:ModelConstants.POSE_WIDTH//2].tolist()
|
||||
#temporal_pose.transStd = net_output_data['sim_pose_stds'][0,:ModelConstants.POSE_WIDTH//2].tolist()
|
||||
#temporal_pose.rot = net_output_data['sim_pose'][0,ModelConstants.POSE_WIDTH//2:].tolist()
|
||||
#temporal_pose.rotStd = net_output_data['sim_pose_stds'][0,ModelConstants.POSE_WIDTH//2:].tolist()
|
||||
|
||||
# poly path
|
||||
fill_xyz_poly(driving_model_data.path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T)
|
||||
|
||||
@@ -26,7 +26,7 @@ from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
from openpilot.common.transformations.model import get_warp_matrix
|
||||
from openpilot.system import sentry
|
||||
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value, get_curvature_from_plan
|
||||
from openpilot.selfdrive.modeld.parse_model_outputs import Parser
|
||||
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
||||
@@ -41,8 +41,8 @@ POLICY_PKL_PATH = Path(__file__).parent / 'models/driving_policy_tinygrad.pkl'
|
||||
VISION_METADATA_PATH = Path(__file__).parent / 'models/driving_vision_metadata.pkl'
|
||||
POLICY_METADATA_PATH = Path(__file__).parent / 'models/driving_policy_metadata.pkl'
|
||||
|
||||
LAT_SMOOTH_SECONDS = 0.0
|
||||
LONG_SMOOTH_SECONDS = 0.0
|
||||
LAT_SMOOTH_SECONDS = 0.1
|
||||
LONG_SMOOTH_SECONDS = 0.3
|
||||
MIN_LAT_CONTROL_SPEED = 0.3
|
||||
|
||||
|
||||
@@ -55,7 +55,11 @@ def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.
|
||||
action_t=long_action_t)
|
||||
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, LONG_SMOOTH_SECONDS)
|
||||
|
||||
desired_curvature = model_output['desired_curvature'][0, 0]
|
||||
desired_curvature = get_curvature_from_plan(plan[:,Plan.T_FROM_CURRENT_EULER][:,2],
|
||||
plan[:,Plan.ORIENTATION_RATE][:,2],
|
||||
ModelConstants.T_IDXS,
|
||||
v_ego,
|
||||
lat_action_t)
|
||||
if v_ego > MIN_LAT_CONTROL_SPEED:
|
||||
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, LAT_SMOOTH_SECONDS)
|
||||
else:
|
||||
@@ -172,7 +176,7 @@ class ModelState:
|
||||
# TODO model only uses last value now
|
||||
self.full_prev_desired_curv[0,:-1] = self.full_prev_desired_curv[0,1:]
|
||||
self.full_prev_desired_curv[0,-1,:] = policy_outputs_dict['desired_curvature'][0, :]
|
||||
self.numpy_inputs['prev_desired_curv'][:] = self.full_prev_desired_curv[0, self.temporal_idxs]
|
||||
self.numpy_inputs['prev_desired_curv'][:] = 0*self.full_prev_desired_curv[0, self.temporal_idxs]
|
||||
|
||||
combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}
|
||||
if SEND_RAW_PRED:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:98f0121ccb6f850077b04cc91bd33d370fc6cbdc2bd35f1ab55628a15a813f36
|
||||
size 15966721
|
||||
oid sha256:19e30484236efff72d519938c3e26461dbeb89c11d81fa7ecbff8e0263333c18
|
||||
size 15588463
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:897f80d0388250f99bba69b6a8434560cc0fd83157cbeb0bc134c67fe4e64624
|
||||
size 34882971
|
||||
oid sha256:dad289ae367cefcb862ef1d707fb4919d008f0eeaa1ebaf18df58d8de5a7d96e
|
||||
size 46265585
|
||||
|
||||
@@ -88,6 +88,12 @@ class Parser:
|
||||
self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
|
||||
self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,))
|
||||
self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
|
||||
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
|
||||
self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
|
||||
self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION,
|
||||
out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH))
|
||||
for k in ['lead_prob', 'lane_lines_prob']:
|
||||
self.parse_binary_crossentropy(k, outs)
|
||||
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH))
|
||||
self.parse_binary_crossentropy('meta', outs)
|
||||
return outs
|
||||
@@ -95,17 +101,10 @@ class Parser:
|
||||
def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||
self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION,
|
||||
out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH))
|
||||
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
|
||||
self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
|
||||
self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
|
||||
self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION,
|
||||
out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH))
|
||||
if 'lat_planner_solution' in outs:
|
||||
self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH))
|
||||
if 'desired_curvature' in outs:
|
||||
self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,))
|
||||
for k in ['lead_prob', 'lane_lines_prob']:
|
||||
self.parse_binary_crossentropy(k, outs)
|
||||
self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,))
|
||||
return outs
|
||||
|
||||
|
||||
@@ -474,7 +474,12 @@ void pandad_run(std::vector<Panda *> &pandas) {
|
||||
for (auto *panda : pandas) {
|
||||
std::string log = panda->serial_read();
|
||||
if (!log.empty()) {
|
||||
LOGD("%s", log.c_str());
|
||||
if (log.find("Register 0x") != std::string::npos) {
|
||||
// Log register divergent faults as errors
|
||||
LOGE("%s", log.c_str());
|
||||
} else {
|
||||
LOGD("%s", log.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
7bf4ae5b92a3ad1f073f675e24e28babad0f2aa0
|
||||
7bf4ae5b92a3ad1f073f675e24e28babad0f2aa0
|
||||
@@ -1 +1 @@
|
||||
dec34c57c4131f6fca5d1035201d1afbf43e5250cede7bfdc798371af008afad
|
||||
8ef2dbcae743eb132167074a374f0a834308be31cffd532598bb13c3d7144a57
|
||||
@@ -845,15 +845,15 @@ def ws_manage(ws: WebSocket, end_event: threading.Event) -> None:
|
||||
onroad_prev = onroad
|
||||
|
||||
if sock is not None:
|
||||
if sys.platform == 'darwin': # macOS
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPALIVE, 7 if onroad else 30)
|
||||
else:
|
||||
# While not sending data, onroad, we can expect to time out in 7 + (7 * 2) = 21s
|
||||
# offroad, we can expect to time out in 30 + (10 * 3) = 60s
|
||||
# FIXME: TCP_USER_TIMEOUT is effectively 2x for some reason (32s), so it's mostly unused
|
||||
# While not sending data, onroad, we can expect to time out in 7 + (7 * 2) = 21s
|
||||
# offroad, we can expect to time out in 30 + (10 * 3) = 60s
|
||||
# FIXME: TCP_USER_TIMEOUT is effectively 2x for some reason (32s), so it's mostly unused
|
||||
if sys.platform == 'linux':
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, 16000 if onroad else 0)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 7 if onroad else 30)
|
||||
elif sys.platform == 'darwin':
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPALIVE, 7 if onroad else 30)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 7 if onroad else 10)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 2 if onroad else 3)
|
||||
|
||||
|
||||
@@ -56,29 +56,29 @@
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-9b07cc366919890cc88bdd45c8c7e643bf66557caf9ad6a1373accc6dcacd892.img.xz",
|
||||
"hash": "9b07cc366919890cc88bdd45c8c7e643bf66557caf9ad6a1373accc6dcacd892",
|
||||
"hash_raw": "9b07cc366919890cc88bdd45c8c7e643bf66557caf9ad6a1373accc6dcacd892",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-3d8e848796924081f5a6b3d745808b1117ae2ec41c03f2d41ee2e75633bd6425.img.xz",
|
||||
"hash": "3d8e848796924081f5a6b3d745808b1117ae2ec41c03f2d41ee2e75633bd6425",
|
||||
"hash_raw": "3d8e848796924081f5a6b3d745808b1117ae2ec41c03f2d41ee2e75633bd6425",
|
||||
"size": 18479104,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "41d31b862fec1b87879b508c405adb9d7b5c0a3324f7350bd904f451605b06cf"
|
||||
"ondevice_hash": "2075104847d1c96a06f07e85efb9f48d0e792d75a059047eae7ba4b463ffeadf"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-02a6f40cc305faf703ab8f993a49d720043e4df1c0787d60dcf87eedb9f2ffde.img.xz",
|
||||
"hash": "c56256a64e6d7e16886e39a4263ffb686ed0f03d3a665c3552f54a39723f8824",
|
||||
"hash_raw": "02a6f40cc305faf703ab8f993a49d720043e4df1c0787d60dcf87eedb9f2ffde",
|
||||
"size": 4404019200,
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-b22bd239c6f9527596fd49b98b7d521a563f99b90953ce021ee36567498e99c7.img.xz",
|
||||
"hash": "cb9bfde1e995b97f728f5d5ad8d7a0f7a01544db5d138ead9b2350f222640939",
|
||||
"hash_raw": "b22bd239c6f9527596fd49b98b7d521a563f99b90953ce021ee36567498e99c7",
|
||||
"size": 5368709120,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "ed2e11f52beb8559223bf9fb989fd4ef5d2ce66eeb11ae0053fff8e41903a533",
|
||||
"ondevice_hash": "e92a1f34158c60364c8d47b8ebbb6e59edf8d4865cd5edfeb2355d6f54f617fc",
|
||||
"alt": {
|
||||
"hash": "02a6f40cc305faf703ab8f993a49d720043e4df1c0787d60dcf87eedb9f2ffde",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-02a6f40cc305faf703ab8f993a49d720043e4df1c0787d60dcf87eedb9f2ffde.img",
|
||||
"size": 4404019200
|
||||
"hash": "b22bd239c6f9527596fd49b98b7d521a563f99b90953ce021ee36567498e99c7",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-b22bd239c6f9527596fd49b98b7d521a563f99b90953ce021ee36567498e99c7.img",
|
||||
"size": 5368709120
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -339,62 +339,62 @@
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-9b07cc366919890cc88bdd45c8c7e643bf66557caf9ad6a1373accc6dcacd892.img.xz",
|
||||
"hash": "9b07cc366919890cc88bdd45c8c7e643bf66557caf9ad6a1373accc6dcacd892",
|
||||
"hash_raw": "9b07cc366919890cc88bdd45c8c7e643bf66557caf9ad6a1373accc6dcacd892",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-3d8e848796924081f5a6b3d745808b1117ae2ec41c03f2d41ee2e75633bd6425.img.xz",
|
||||
"hash": "3d8e848796924081f5a6b3d745808b1117ae2ec41c03f2d41ee2e75633bd6425",
|
||||
"hash_raw": "3d8e848796924081f5a6b3d745808b1117ae2ec41c03f2d41ee2e75633bd6425",
|
||||
"size": 18479104,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "41d31b862fec1b87879b508c405adb9d7b5c0a3324f7350bd904f451605b06cf"
|
||||
"ondevice_hash": "2075104847d1c96a06f07e85efb9f48d0e792d75a059047eae7ba4b463ffeadf"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-02a6f40cc305faf703ab8f993a49d720043e4df1c0787d60dcf87eedb9f2ffde.img.xz",
|
||||
"hash": "c56256a64e6d7e16886e39a4263ffb686ed0f03d3a665c3552f54a39723f8824",
|
||||
"hash_raw": "02a6f40cc305faf703ab8f993a49d720043e4df1c0787d60dcf87eedb9f2ffde",
|
||||
"size": 4404019200,
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-b22bd239c6f9527596fd49b98b7d521a563f99b90953ce021ee36567498e99c7.img.xz",
|
||||
"hash": "cb9bfde1e995b97f728f5d5ad8d7a0f7a01544db5d138ead9b2350f222640939",
|
||||
"hash_raw": "b22bd239c6f9527596fd49b98b7d521a563f99b90953ce021ee36567498e99c7",
|
||||
"size": 5368709120,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "ed2e11f52beb8559223bf9fb989fd4ef5d2ce66eeb11ae0053fff8e41903a533",
|
||||
"ondevice_hash": "e92a1f34158c60364c8d47b8ebbb6e59edf8d4865cd5edfeb2355d6f54f617fc",
|
||||
"alt": {
|
||||
"hash": "02a6f40cc305faf703ab8f993a49d720043e4df1c0787d60dcf87eedb9f2ffde",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-02a6f40cc305faf703ab8f993a49d720043e4df1c0787d60dcf87eedb9f2ffde.img",
|
||||
"size": 4404019200
|
||||
"hash": "b22bd239c6f9527596fd49b98b7d521a563f99b90953ce021ee36567498e99c7",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-b22bd239c6f9527596fd49b98b7d521a563f99b90953ce021ee36567498e99c7.img",
|
||||
"size": 5368709120
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "userdata_90",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_90-175a5d3353daa5e7b7d9939cb51a2f1d7e6312b4708ad654c351da2f1ef4f108.img.xz",
|
||||
"hash": "ff01a0ca5a2ea6661f836248043a211cd8d71c3269c139cb574b56855fabc3f4",
|
||||
"hash_raw": "175a5d3353daa5e7b7d9939cb51a2f1d7e6312b4708ad654c351da2f1ef4f108",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_90-4bb7239f7e82c846e4d2584c0c433f03c582a80950de4094e6c190563d6d84ac.img.xz",
|
||||
"hash": "b18001a2a87caa070fabf6321f8215ac353d6444564e3f86329b4dccc039ce54",
|
||||
"hash_raw": "4bb7239f7e82c846e4d2584c0c433f03c582a80950de4094e6c190563d6d84ac",
|
||||
"size": 96636764160,
|
||||
"sparse": true,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "2f3d69e5015a45a18c3553f2edc5706aacd6d84a4b3d5010a3d76a1a3aa910b0"
|
||||
"ondevice_hash": "15ce16f2349d5b4d5fec6ad1e36222b1ae744ed10b8930bc9af75bd244dccb3c"
|
||||
},
|
||||
{
|
||||
"name": "userdata_89",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_89-61bdaf82d3036af6e45e86adbaab02918b41debd5b58b6708d7987084d514d1b.img.xz",
|
||||
"hash": "714970777e02bb53a71640735bdb84b3071ecbc0346b978ce12eb667d75634ec",
|
||||
"hash_raw": "61bdaf82d3036af6e45e86adbaab02918b41debd5b58b6708d7987084d514d1b",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_89-e36b59bf9ff755b6ca488df2ba1e20da8f7dab6b8843129f3fdcccd7ff2ff7d8.img.xz",
|
||||
"hash": "12682cf54596ab1bd1c2464c4ca85888e4e06b47af5ff7d0432399e9907e2f64",
|
||||
"hash_raw": "e36b59bf9ff755b6ca488df2ba1e20da8f7dab6b8843129f3fdcccd7ff2ff7d8",
|
||||
"size": 95563022336,
|
||||
"sparse": true,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "95e6889a808b8d266660990e67e917cf3b63179f23588565af7f2fa54f70ac76"
|
||||
"ondevice_hash": "e4df9dea47ff04967d971263d50c17460ef240457e8d814e7c4f409f7493eb8a"
|
||||
},
|
||||
{
|
||||
"name": "userdata_30",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_30-a40553d3fd339cb0107f1cc55fd532820f192a7a9a90d05243ad00fcbf804997.img.xz",
|
||||
"hash": "33e5ab398620f147b885a9627b2608591bd9e1c9aa481eb705dc86707d706ea2",
|
||||
"hash_raw": "a40553d3fd339cb0107f1cc55fd532820f192a7a9a90d05243ad00fcbf804997",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_30-fe1d86f5322c675c58b3ae9753a4670abf44a25746bf6ac822aed108bb577282.img.xz",
|
||||
"hash": "fa471703be0f0647617d183312d5209d23407f1628e4ab0934e6ec54b1a6b263",
|
||||
"hash_raw": "fe1d86f5322c675c58b3ae9753a4670abf44a25746bf6ac822aed108bb577282",
|
||||
"size": 32212254720,
|
||||
"sparse": true,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "cd6291dea40968123f7af0b831cbfbbd6e515b676f2e427ae47ff358f6ac148e"
|
||||
"ondevice_hash": "0b5b2402c9caa1ed7b832818e66580c974251e735bda91f2f226c41499d5616e"
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -56,10 +56,10 @@ class TestPowerDraw:
|
||||
def valid_msg_count(self, proc, msg_counts):
|
||||
msgs_received = sum(msg_counts[msg] for msg in proc.msgs)
|
||||
msgs_expected = self.get_expected_messages(proc)
|
||||
return np.core.numeric.isclose(msgs_expected, msgs_received, rtol=.02, atol=2)
|
||||
return np.isclose(msgs_expected, msgs_received, rtol=.02, atol=2)
|
||||
|
||||
def valid_power_draw(self, proc, used):
|
||||
return np.core.numeric.isclose(used, proc.power, rtol=proc.rtol, atol=proc.atol)
|
||||
return np.isclose(used, proc.power, rtol=proc.rtol, atol=proc.atol)
|
||||
|
||||
def tabulate_msg_counts(self, msgs_and_power):
|
||||
msg_counts = defaultdict(int)
|
||||
|
||||
@@ -203,7 +203,7 @@ void handle_user_flag(LoggerdState *s) {
|
||||
|
||||
// mark route for uploading
|
||||
Params params;
|
||||
std::string routes = Params().get("AthenadRecentlyViewedRoutes");
|
||||
std::string routes = params.get("AthenadRecentlyViewedRoutes");
|
||||
params.put("AthenadRecentlyViewedRoutes", routes + "," + s->logger.routeName());
|
||||
|
||||
prev_segment = s->logger.segment();
|
||||
|
||||
@@ -89,7 +89,7 @@ class Uploader:
|
||||
|
||||
def list_upload_files(self, metered: bool) -> Iterator[tuple[str, str, str]]:
|
||||
r = self.params.get("AthenadRecentlyViewedRoutes", encoding="utf8")
|
||||
requested_routes = [] if r is None else r.split(",")
|
||||
requested_routes = [] if r is None else [route for route in r.split(",") if route]
|
||||
|
||||
for logdir in listdir_by_creation(self.root):
|
||||
path = os.path.join(self.root, logdir)
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import os
|
||||
import errno
|
||||
|
||||
import xattr
|
||||
|
||||
_cached_attributes: dict[tuple, bytes | None] = {}
|
||||
|
||||
def getxattr(path: str, attr_name: str) -> bytes | None:
|
||||
key = (path, attr_name)
|
||||
if key not in _cached_attributes:
|
||||
try:
|
||||
response = os.getxattr(path, attr_name)
|
||||
response = xattr.getxattr(path, attr_name)
|
||||
except OSError as e:
|
||||
# ENODATA means attribute hasn't been set
|
||||
if e.errno == errno.ENODATA:
|
||||
# ENODATA (Linux) or ENOATTR (macOS) means attribute hasn't been set
|
||||
if e.errno == errno.ENODATA or (hasattr(errno, 'ENOATTR') and e.errno == errno.ENOATTR):
|
||||
response = None
|
||||
else:
|
||||
raise
|
||||
@@ -19,4 +20,4 @@ def getxattr(path: str, attr_name: str) -> bytes | None:
|
||||
|
||||
def setxattr(path: str, attr_name: str, attr_value: bytes) -> None:
|
||||
_cached_attributes.pop((path, attr_name), None)
|
||||
return os.setxattr(path, attr_name, attr_value)
|
||||
xattr.setxattr(path, attr_name, attr_value)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import numpy as np
|
||||
from functools import cache
|
||||
import threading
|
||||
|
||||
from cereal import messaging
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
@@ -52,12 +53,18 @@ class Mic:
|
||||
self.sound_pressure_weighted = 0
|
||||
self.sound_pressure_level_weighted = 0
|
||||
|
||||
def update(self):
|
||||
msg = messaging.new_message('microphone', valid=True)
|
||||
msg.microphone.soundPressure = float(self.sound_pressure)
|
||||
msg.microphone.soundPressureWeighted = float(self.sound_pressure_weighted)
|
||||
self.lock = threading.Lock()
|
||||
|
||||
msg.microphone.soundPressureWeightedDb = float(self.sound_pressure_level_weighted)
|
||||
def update(self):
|
||||
with self.lock:
|
||||
sound_pressure = self.sound_pressure
|
||||
sound_pressure_weighted = self.sound_pressure_weighted
|
||||
sound_pressure_level_weighted = self.sound_pressure_level_weighted
|
||||
|
||||
msg = messaging.new_message('microphone', valid=True)
|
||||
msg.microphone.soundPressure = float(sound_pressure)
|
||||
msg.microphone.soundPressureWeighted = float(sound_pressure_weighted)
|
||||
msg.microphone.soundPressureWeightedDb = float(sound_pressure_level_weighted)
|
||||
|
||||
self.pm.send('microphone', msg)
|
||||
self.rk.keep_time()
|
||||
@@ -69,17 +76,17 @@ class Mic:
|
||||
|
||||
Logged A-weighted equivalents are rough approximations of the human-perceived loudness.
|
||||
"""
|
||||
with self.lock:
|
||||
self.measurements = np.concatenate((self.measurements, indata[:, 0]))
|
||||
|
||||
self.measurements = np.concatenate((self.measurements, indata[:, 0]))
|
||||
while self.measurements.size >= FFT_SAMPLES:
|
||||
measurements = self.measurements[:FFT_SAMPLES]
|
||||
|
||||
while self.measurements.size >= FFT_SAMPLES:
|
||||
measurements = self.measurements[:FFT_SAMPLES]
|
||||
self.sound_pressure, _ = calculate_spl(measurements)
|
||||
measurements_weighted = apply_a_weighting(measurements)
|
||||
self.sound_pressure_weighted, self.sound_pressure_level_weighted = calculate_spl(measurements_weighted)
|
||||
|
||||
self.sound_pressure, _ = calculate_spl(measurements)
|
||||
measurements_weighted = apply_a_weighting(measurements)
|
||||
self.sound_pressure_weighted, self.sound_pressure_level_weighted = calculate_spl(measurements_weighted)
|
||||
|
||||
self.measurements = self.measurements[FFT_SAMPLES:]
|
||||
self.measurements = self.measurements[FFT_SAMPLES:]
|
||||
|
||||
@retry(attempts=7, delay=3)
|
||||
def get_stream(self, sd):
|
||||
|
||||
@@ -7,7 +7,7 @@ from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
|
||||
DEFAULT_FPS = 30
|
||||
DEFAULT_FPS = 60
|
||||
FPS_LOG_INTERVAL = 5 # Seconds between logging FPS drops
|
||||
FPS_DROP_THRESHOLD = 0.9 # FPS drop threshold for triggering a warning
|
||||
FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions
|
||||
|
||||
@@ -4,6 +4,7 @@ from enum import IntEnum
|
||||
MOUSE_WHEEL_SCROLL_SPEED = 30
|
||||
INERTIA_FRICTION = 0.95 # The rate at which the inertia slows down
|
||||
MIN_VELOCITY = 0.1 # Minimum velocity before stopping the inertia
|
||||
DRAG_THRESHOLD = 5 # Pixels of movement to consider it a drag, not a click
|
||||
|
||||
|
||||
class ScrollState(IntEnum):
|
||||
@@ -16,10 +17,12 @@ class GuiScrollPanel:
|
||||
def __init__(self, show_vertical_scroll_bar: bool = False):
|
||||
self._scroll_state: ScrollState = ScrollState.IDLE
|
||||
self._last_mouse_y: float = 0.0
|
||||
self._start_mouse_y: float = 0.0 # Track the initial mouse position for drag detection
|
||||
self._offset = rl.Vector2(0, 0)
|
||||
self._view = rl.Rectangle(0, 0, 0, 0)
|
||||
self._show_vertical_scroll_bar: bool = show_vertical_scroll_bar
|
||||
self._velocity_y = 0.0 # Velocity for inertia
|
||||
self._is_dragging = False
|
||||
|
||||
def handle_scroll(self, bounds: rl.Rectangle, content: rl.Rectangle) -> rl.Vector2:
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
@@ -35,20 +38,27 @@ class GuiScrollPanel:
|
||||
self._scroll_state = ScrollState.DRAGGING_SCROLLBAR
|
||||
|
||||
self._last_mouse_y = mouse_pos.y
|
||||
self._start_mouse_y = mouse_pos.y # Record starting position
|
||||
self._velocity_y = 0.0 # Reset velocity when drag starts
|
||||
self._is_dragging = False # Reset dragging flag
|
||||
|
||||
if self._scroll_state != ScrollState.IDLE:
|
||||
if rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||
delta_y = mouse_pos.y - self._last_mouse_y
|
||||
|
||||
# Check if movement exceeds the drag threshold
|
||||
total_drag = abs(mouse_pos.y - self._start_mouse_y)
|
||||
if total_drag > DRAG_THRESHOLD:
|
||||
self._is_dragging = True
|
||||
|
||||
if self._scroll_state == ScrollState.DRAGGING_CONTENT:
|
||||
self._offset.y += delta_y
|
||||
else:
|
||||
elif self._scroll_state == ScrollState.DRAGGING_SCROLLBAR:
|
||||
delta_y = -delta_y
|
||||
|
||||
self._last_mouse_y = mouse_pos.y
|
||||
self._velocity_y = delta_y # Update velocity during drag
|
||||
else:
|
||||
elif rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT):
|
||||
self._scroll_state = ScrollState.IDLE
|
||||
|
||||
# Handle mouse wheel scrolling
|
||||
@@ -73,3 +83,6 @@ class GuiScrollPanel:
|
||||
self._offset.y = max(min(self._offset.y, 0), -max_scroll_y)
|
||||
|
||||
return self._offset
|
||||
|
||||
def is_click_valid(self) -> bool:
|
||||
return self._scroll_state == ScrollState.IDLE and not self._is_dragging and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
||||
|
||||
53
system/ui/lib/toggle.py
Normal file
53
system/ui/lib/toggle.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import pyray as rl
|
||||
|
||||
ON_COLOR = rl.GREEN
|
||||
OFF_COLOR = rl.Color(0x39, 0x39, 0x39, 255)
|
||||
KNOB_COLOR = rl.WHITE
|
||||
BG_HEIGHT = 60
|
||||
KNOB_HEIGHT = 80
|
||||
WIDTH = 160
|
||||
|
||||
|
||||
class Toggle:
|
||||
def __init__(self, x, y, initial_state=False):
|
||||
self._state = initial_state
|
||||
self._rect = rl.Rectangle(x, y, WIDTH, KNOB_HEIGHT)
|
||||
|
||||
def handle_input(self):
|
||||
if rl.is_mouse_button_pressed(rl.MOUSE_LEFT_BUTTON):
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
if rl.check_collision_point_rec(mouse_pos, self._rect):
|
||||
self._state = not self._state
|
||||
|
||||
def get_state(self):
|
||||
return self._state
|
||||
|
||||
def render(self):
|
||||
self._draw_background()
|
||||
self._draw_knob()
|
||||
|
||||
def _draw_background(self):
|
||||
bg_rect = rl.Rectangle(
|
||||
self._rect.x + 5,
|
||||
self._rect.y + (KNOB_HEIGHT - BG_HEIGHT) / 2,
|
||||
self._rect.width - 10,
|
||||
BG_HEIGHT,
|
||||
)
|
||||
rl.draw_rectangle_rounded(bg_rect, 1.0, 10, ON_COLOR if self._state else OFF_COLOR)
|
||||
|
||||
def _draw_knob(self):
|
||||
knob_radius = KNOB_HEIGHT / 2
|
||||
knob_x = self._rect.x + knob_radius if not self._state else self._rect.x + self._rect.width - knob_radius
|
||||
knob_y = self._rect.y + knob_radius
|
||||
rl.draw_circle(int(knob_x), int(knob_y), knob_radius, KNOB_COLOR)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
|
||||
gui_app.init_window("Text toggle example")
|
||||
toggle = Toggle(100, 100)
|
||||
for _ in gui_app.render():
|
||||
toggle.handle_input()
|
||||
toggle.render()
|
||||
|
||||
481
system/ui/lib/wifi_manager.py
Normal file
481
system/ui/lib/wifi_manager.py
Normal file
@@ -0,0 +1,481 @@
|
||||
import asyncio
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
|
||||
from dbus_next.aio import MessageBus
|
||||
from dbus_next import BusType, Variant, Message
|
||||
from dbus_next.errors import DBusError
|
||||
from dbus_next.constants import MessageType
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
# NetworkManager constants
|
||||
NM = "org.freedesktop.NetworkManager"
|
||||
NM_PATH = '/org/freedesktop/NetworkManager'
|
||||
NM_IFACE = 'org.freedesktop.NetworkManager'
|
||||
NM_SETTINGS_PATH = '/org/freedesktop/NetworkManager/Settings'
|
||||
NM_SETTINGS_IFACE = 'org.freedesktop.NetworkManager.Settings'
|
||||
NM_CONNECTION_IFACE = 'org.freedesktop.NetworkManager.Settings.Connection'
|
||||
NM_WIRELESS_IFACE = 'org.freedesktop.NetworkManager.Device.Wireless'
|
||||
NM_PROPERTIES_IFACE = 'org.freedesktop.DBus.Properties'
|
||||
NM_DEVICE_IFACE = "org.freedesktop.NetworkManager.Device"
|
||||
|
||||
NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8
|
||||
|
||||
# NetworkManager device states
|
||||
class NMDeviceState(IntEnum):
|
||||
DISCONNECTED = 30
|
||||
PREPARE = 40
|
||||
NEED_AUTH = 60
|
||||
IP_CONFIG = 70
|
||||
ACTIVATED = 100
|
||||
|
||||
class SecurityType(IntEnum):
|
||||
OPEN = 0
|
||||
WPA = 1
|
||||
WPA2 = 2
|
||||
WPA3 = 3
|
||||
UNSUPPORTED = 4
|
||||
|
||||
@dataclass
|
||||
class NetworkInfo:
|
||||
ssid: str
|
||||
strength: int
|
||||
is_connected: bool
|
||||
security_type: SecurityType
|
||||
path: str
|
||||
bssid: str
|
||||
# saved_path: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class WifiManagerCallbacks:
|
||||
need_auth: Callable[[], None] | None = None
|
||||
activated: Callable[[], None] | None = None
|
||||
forgotten: Callable[[], None] | None = None
|
||||
|
||||
|
||||
class WifiManager:
|
||||
def __init__(self, callbacks):
|
||||
self.callbacks: WifiManagerCallbacks = callbacks
|
||||
self.networks: list[NetworkInfo] = []
|
||||
self.bus: MessageBus = None
|
||||
self.device_path: str = ""
|
||||
self.device_proxy = None
|
||||
self.saved_connections: dict[str, str] = {}
|
||||
self.active_ap_path: str = ""
|
||||
self.scan_task: asyncio.Task | None = None
|
||||
self.running: bool = True
|
||||
|
||||
async def connect(self) -> None:
|
||||
"""Connect to the DBus system bus."""
|
||||
try:
|
||||
self.bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
|
||||
if not await self._find_wifi_device():
|
||||
raise ValueError("No Wi-Fi device found")
|
||||
await self._setup_signals(self.device_path)
|
||||
|
||||
self.active_ap_path = await self.get_active_access_point()
|
||||
self.saved_connections = await self._get_saved_connections()
|
||||
self.scan_task = asyncio.create_task(self._periodic_scan())
|
||||
except DBusError as e:
|
||||
cloudlog.error(f"Failed to connect to DBus: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
cloudlog.error(f"Unexpected error during connect: {e}")
|
||||
raise
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
self.running = False
|
||||
if self.scan_task:
|
||||
self.scan_task.cancel()
|
||||
try:
|
||||
await self.scan_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
if self.bus:
|
||||
await self.bus.disconnect()
|
||||
|
||||
async def request_scan(self) -> None:
|
||||
try:
|
||||
interface = self.device_proxy.get_interface(NM_WIRELESS_IFACE)
|
||||
await interface.call_request_scan({})
|
||||
except DBusError as e:
|
||||
cloudlog.warning(f"Scan request failed: {str(e)}")
|
||||
|
||||
async def get_active_access_point(self):
|
||||
try:
|
||||
props_iface = self.device_proxy.get_interface(NM_PROPERTIES_IFACE)
|
||||
ap_path = await props_iface.call_get(NM_WIRELESS_IFACE, 'ActiveAccessPoint')
|
||||
return ap_path.value
|
||||
except DBusError as e:
|
||||
cloudlog.error(f"Error fetching active access point: {str(e)}")
|
||||
return ''
|
||||
|
||||
async def forget_connection(self, ssid: str) -> bool:
|
||||
path = self.saved_connections.get(ssid)
|
||||
if not path:
|
||||
return False
|
||||
|
||||
try:
|
||||
nm_iface = await self._get_interface(NM, path, NM_CONNECTION_IFACE)
|
||||
await nm_iface.call_delete()
|
||||
return True
|
||||
except DBusError as e:
|
||||
cloudlog.error(f"Failed to delete connection for SSID: {ssid}. Error: {e}")
|
||||
return False
|
||||
|
||||
async def activate_connection(self, ssid: str) -> bool:
|
||||
connection_path = self.saved_connections.get(ssid)
|
||||
if not connection_path:
|
||||
return False
|
||||
try:
|
||||
nm_iface = await self._get_interface(NM, NM_PATH, NM_IFACE)
|
||||
await nm_iface.call_activate_connection(connection_path, self.device_path, "/")
|
||||
return True
|
||||
except DBusError as e:
|
||||
cloudlog.error(f"Failed to activate connection {ssid}: {str(e)}")
|
||||
return False
|
||||
|
||||
async def connect_to_network(self, ssid: str, password: str = None, bssid: str = None, is_hidden: bool = False) -> None:
|
||||
"""Connect to a selected Wi-Fi network."""
|
||||
try:
|
||||
connection = {
|
||||
'connection': {
|
||||
'type': Variant('s', '802-11-wireless'),
|
||||
'uuid': Variant('s', str(uuid.uuid4())),
|
||||
'id': Variant('s', ssid),
|
||||
'autoconnect-retries': Variant('i', 0),
|
||||
},
|
||||
'802-11-wireless': {
|
||||
'ssid': Variant('ay', ssid.encode('utf-8')),
|
||||
'hidden': Variant('b', is_hidden),
|
||||
'mode': Variant('s', 'infrastructure'),
|
||||
},
|
||||
'ipv4': {'method': Variant('s', 'auto')},
|
||||
'ipv6': {'method': Variant('s', 'ignore')},
|
||||
}
|
||||
|
||||
if bssid:
|
||||
connection['802-11-wireless']['bssid'] = Variant('ay', bssid.encode('utf-8'))
|
||||
|
||||
if password:
|
||||
connection['802-11-wireless-security'] = {
|
||||
'key-mgmt': Variant('s', 'wpa-psk'),
|
||||
'auth-alg': Variant('s', 'open'),
|
||||
'psk': Variant('s', password),
|
||||
}
|
||||
|
||||
nm_iface = await self._get_interface(NM, NM_PATH, NM_IFACE)
|
||||
await nm_iface.call_add_and_activate_connection(connection, self.device_path, "/")
|
||||
await self._update_connection_status()
|
||||
|
||||
except DBusError as e:
|
||||
cloudlog.error(f"Error connecting to network: {e}")
|
||||
|
||||
def is_saved(self, ssid: str) -> bool:
|
||||
return ssid in self.saved_connections
|
||||
|
||||
async def _find_wifi_device(self) -> bool:
|
||||
nm_iface = await self._get_interface(NM, NM_PATH, NM_IFACE)
|
||||
devices = await nm_iface.get_devices()
|
||||
|
||||
for device_path in devices:
|
||||
device = await self.bus.introspect(NM, device_path)
|
||||
device_proxy = self.bus.get_proxy_object(NM, device_path, device)
|
||||
device_interface = device_proxy.get_interface(NM_DEVICE_IFACE)
|
||||
device_type = await device_interface.get_device_type() # type: ignore[attr-defined]
|
||||
if device_type == 2: # Wi-Fi device
|
||||
self.device_path = device_path
|
||||
self.device_proxy = device_proxy
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def _periodic_scan(self):
|
||||
while self.running:
|
||||
try:
|
||||
await self.request_scan()
|
||||
await self._get_available_networks()
|
||||
await asyncio.sleep(30)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except DBusError as e:
|
||||
cloudlog.error(f"Scan failed: {e}")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def _setup_signals(self, device_path: str) -> None:
|
||||
rules = [
|
||||
f"type='signal',interface='{NM_PROPERTIES_IFACE}',member='PropertiesChanged',path='{device_path}'",
|
||||
f"type='signal',interface='{NM_DEVICE_IFACE}',member='StateChanged',path='{device_path}'",
|
||||
f"type='signal',interface='{NM_SETTINGS_IFACE}',member='NewConnection',path='{NM_SETTINGS_PATH}'",
|
||||
f"type='signal',interface='{NM_SETTINGS_IFACE}',member='ConnectionRemoved',path='{NM_SETTINGS_PATH}'",
|
||||
]
|
||||
for rule in rules:
|
||||
await self._add_match_rule(rule)
|
||||
|
||||
# Set up signal handlers
|
||||
self.device_proxy.get_interface(NM_PROPERTIES_IFACE).on_properties_changed(self._on_properties_changed)
|
||||
self.device_proxy.get_interface(NM_DEVICE_IFACE).on_state_changed(self._on_state_changed)
|
||||
|
||||
settings_iface = await self._get_interface(NM, NM_SETTINGS_PATH, NM_SETTINGS_IFACE)
|
||||
settings_iface.on_new_connection(self._on_new_connection)
|
||||
settings_iface.on_connection_removed(self._on_connection_removed)
|
||||
|
||||
def _on_properties_changed(self, interface: str, changed: dict, invalidated: list):
|
||||
# print("property changed", interface, changed, invalidated)
|
||||
if 'LastScan' in changed:
|
||||
asyncio.create_task(self._get_available_networks())
|
||||
elif interface == NM_WIRELESS_IFACE and "ActiveAccessPoint" in changed:
|
||||
self.active_ap_path = changed["ActiveAccessPoint"].value
|
||||
asyncio.create_task(self._get_available_networks())
|
||||
|
||||
def _on_state_changed(self, new_state: int, old_state: int, reason: int):
|
||||
print(f"State changed: {old_state} -> {new_state}, reason: {reason}")
|
||||
if new_state == NMDeviceState.ACTIVATED:
|
||||
if self.callbacks.activated:
|
||||
self.callbacks.activated()
|
||||
asyncio.create_task(self._update_connection_status())
|
||||
elif new_state in (NMDeviceState.DISCONNECTED, NMDeviceState.NEED_AUTH):
|
||||
for network in self.networks:
|
||||
network.is_connected = False
|
||||
if new_state == NMDeviceState.NEED_AUTH and reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT and self.callbacks.need_auth:
|
||||
self.callbacks.need_auth()
|
||||
|
||||
def _on_new_connection(self, path: str) -> None:
|
||||
"""Callback for NewConnection signal."""
|
||||
print(f"New connection added: {path}")
|
||||
asyncio.create_task(self._add_saved_connection(path))
|
||||
|
||||
def _on_connection_removed(self, path: str) -> None:
|
||||
"""Callback for ConnectionRemoved signal."""
|
||||
print(f"Connection removed: {path}")
|
||||
for ssid, p in list(self.saved_connections.items()):
|
||||
if path == p:
|
||||
del self.saved_connections[ssid]
|
||||
if self.callbacks.forgotten:
|
||||
self.callbacks.forgotten()
|
||||
break
|
||||
|
||||
async def _add_saved_connection(self, path: str) -> None:
|
||||
"""Add a new saved connection to the dictionary."""
|
||||
try:
|
||||
settings = await self._get_connection_settings(path)
|
||||
if ssid := self._extract_ssid(settings):
|
||||
self.saved_connections[ssid] = path
|
||||
except DBusError as e:
|
||||
cloudlog.error(f"Failed to add connection {path}: {e}")
|
||||
|
||||
def _extract_ssid(self, settings: dict) -> str | None:
|
||||
"""Extract SSID from connection settings."""
|
||||
ssid_variant = settings.get('802-11-wireless', {}).get('ssid', Variant('ay', b'')).value
|
||||
return ''.join(chr(b) for b in ssid_variant) if ssid_variant else None
|
||||
|
||||
async def _update_connection_status(self):
|
||||
self.active_ap_path = await self.get_active_access_point()
|
||||
await self._get_available_networks()
|
||||
|
||||
async def _add_match_rule(self, rule):
|
||||
"""Add a match rule on the bus."""
|
||||
reply = await self.bus.call(
|
||||
Message(
|
||||
message_type=MessageType.METHOD_CALL,
|
||||
destination='org.freedesktop.DBus',
|
||||
interface="org.freedesktop.DBus",
|
||||
path='/org/freedesktop/DBus',
|
||||
member='AddMatch',
|
||||
signature='s',
|
||||
body=[rule],
|
||||
)
|
||||
)
|
||||
|
||||
assert reply.message_type == MessageType.METHOD_RETURN
|
||||
return reply
|
||||
|
||||
async def _get_available_networks(self):
|
||||
"""Get a list of available networks via NetworkManager."""
|
||||
wifi_iface = self.device_proxy.get_interface(NM_WIRELESS_IFACE)
|
||||
access_points = await wifi_iface.get_access_points()
|
||||
network_dict = {}
|
||||
for ap_path in access_points:
|
||||
try:
|
||||
props_iface = await self._get_interface(NM, ap_path, NM_PROPERTIES_IFACE)
|
||||
properties = await props_iface.call_get_all('org.freedesktop.NetworkManager.AccessPoint')
|
||||
ssid_variant = properties['Ssid'].value
|
||||
ssid = ''.join(chr(byte) for byte in ssid_variant)
|
||||
if not ssid:
|
||||
continue
|
||||
|
||||
bssid = properties.get('HwAddress', Variant('s', '')).value
|
||||
strength = properties['Strength'].value
|
||||
flags = properties['Flags'].value
|
||||
wpa_flags = properties['WpaFlags'].value
|
||||
rsn_flags = properties['RsnFlags'].value
|
||||
existing_network = network_dict.get(ssid)
|
||||
if not existing_network or ((not existing_network.bssid and bssid) or (existing_network.strength < strength)):
|
||||
network_dict[ssid] = NetworkInfo(
|
||||
ssid=ssid,
|
||||
strength=strength,
|
||||
security_type=self._get_security_type(flags, wpa_flags, rsn_flags),
|
||||
path=ap_path,
|
||||
bssid=bssid,
|
||||
is_connected=self.active_ap_path == ap_path,
|
||||
)
|
||||
|
||||
except DBusError as e:
|
||||
cloudlog.error(f"Error fetching networks: {e}")
|
||||
except Exception as e:
|
||||
cloudlog.error({e})
|
||||
|
||||
self.networks = sorted(
|
||||
network_dict.values(),
|
||||
key=lambda network: (
|
||||
not network.is_connected,
|
||||
-network.strength, # Higher signal strength first
|
||||
network.ssid.lower(),
|
||||
),
|
||||
)
|
||||
|
||||
async def _get_connection_settings(self, path):
|
||||
"""Fetch connection settings for a specific connection path."""
|
||||
try:
|
||||
connection_proxy = await self.bus.introspect(NM, path)
|
||||
connection = self.bus.get_proxy_object(NM, path, connection_proxy)
|
||||
settings = connection.get_interface(NM_CONNECTION_IFACE)
|
||||
return await settings.call_get_settings()
|
||||
except DBusError as e:
|
||||
cloudlog.error(f"Failed to get settings for {path}: {str(e)}")
|
||||
return {}
|
||||
|
||||
async def _process_chunk(self, paths_chunk):
|
||||
"""Process a chunk of connection paths."""
|
||||
tasks = [self._get_connection_settings(path) for path in paths_chunk]
|
||||
return await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
async def _get_saved_connections(self) -> dict[str, str]:
|
||||
try:
|
||||
settings_iface = await self._get_interface(NM, NM_SETTINGS_PATH, NM_SETTINGS_IFACE)
|
||||
connection_paths = await settings_iface.call_list_connections()
|
||||
saved_ssids: dict[str, str] = {}
|
||||
batch_size = 20
|
||||
for i in range(0, len(connection_paths), batch_size):
|
||||
chunk = connection_paths[i : i + batch_size]
|
||||
results = await self._process_chunk(chunk)
|
||||
for path, config in zip(chunk, results, strict=True):
|
||||
if isinstance(config, dict) and '802-11-wireless' in config:
|
||||
if ssid := self._extract_ssid(config):
|
||||
saved_ssids[ssid] = path
|
||||
return saved_ssids
|
||||
except DBusError as e:
|
||||
cloudlog.error(f"Error fetching saved connections: {str(e)}")
|
||||
return {}
|
||||
|
||||
async def _get_interface(self, bus_name: str, path: str, name: str):
|
||||
introspection = await self.bus.introspect(bus_name, path)
|
||||
proxy = self.bus.get_proxy_object(bus_name, path, introspection)
|
||||
return proxy.get_interface(name)
|
||||
|
||||
def _get_security_type(self, flags: int, wpa_flags: int, rsn_flags: int) -> SecurityType:
|
||||
"""Determine the security type based on flags."""
|
||||
if flags == 0 and not (wpa_flags or rsn_flags):
|
||||
return SecurityType.OPEN
|
||||
if rsn_flags & 0x200: # SAE (WPA3 Personal)
|
||||
return SecurityType.WPA3
|
||||
if rsn_flags: # RSN indicates WPA2 or higher
|
||||
return SecurityType.WPA2
|
||||
if wpa_flags: # WPA flags indicate WPA
|
||||
return SecurityType.WPA
|
||||
return SecurityType.UNSUPPORTED
|
||||
|
||||
|
||||
class WifiManagerWrapper:
|
||||
def __init__(self):
|
||||
self._manager: WifiManager | None = None
|
||||
self._callbacks: WifiManagerCallbacks = WifiManagerCallbacks()
|
||||
|
||||
self._thread = threading.Thread(target=self._run, daemon=True)
|
||||
self._loop: asyncio.EventLoop | None = None
|
||||
self._running = False
|
||||
|
||||
def set_callbacks(self, callbacks: WifiManagerCallbacks):
|
||||
self._callbacks = callbacks
|
||||
|
||||
def start(self) -> None:
|
||||
if not self._running:
|
||||
self._thread.start()
|
||||
while self._thread is not None and not self._running:
|
||||
time.sleep(0.1)
|
||||
|
||||
def _run(self):
|
||||
self._loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self._loop)
|
||||
|
||||
try:
|
||||
self._manager = WifiManager(self._callbacks)
|
||||
self._running = True
|
||||
self._loop.run_forever()
|
||||
except Exception as e:
|
||||
cloudlog.error(f"Error in WifiManagerWrapper thread: {e}")
|
||||
finally:
|
||||
if self._loop.is_running():
|
||||
self._loop.stop()
|
||||
self._running = False
|
||||
|
||||
def shutdown(self) -> None:
|
||||
if self._running:
|
||||
if self._manager is not None:
|
||||
self._run_coroutine(self._manager.shutdown())
|
||||
if self._loop and self._loop.is_running():
|
||||
self._loop.call_soon_threadsafe(self._loop.stop)
|
||||
if self._thread and self._thread.is_alive():
|
||||
self._thread.join(timeout=2.0)
|
||||
self._running = False
|
||||
|
||||
@property
|
||||
def networks(self) -> list[NetworkInfo]:
|
||||
"""Get the current list of networks."""
|
||||
return self._manager.networks if self._manager else []
|
||||
|
||||
def is_saved(self, ssid: str) -> bool:
|
||||
"""Check if a network is saved."""
|
||||
return self._manager.is_saved(ssid) if self._manager else False
|
||||
|
||||
def connect(self):
|
||||
"""Connect to DBus and start Wi-Fi scanning."""
|
||||
if not self._manager:
|
||||
return
|
||||
self._run_coroutine(self._manager.connect())
|
||||
|
||||
def request_scan(self):
|
||||
"""Request a scan for Wi-Fi networks."""
|
||||
if not self._manager:
|
||||
return
|
||||
self._run_coroutine(self._manager.request_scan())
|
||||
|
||||
def forget_connection(self, ssid: str):
|
||||
"""Forget a saved Wi-Fi connection."""
|
||||
if not self._manager:
|
||||
return
|
||||
self._run_coroutine(self._manager.forget_connection(ssid))
|
||||
|
||||
def activate_connection(self, ssid: str):
|
||||
"""Activate an existing Wi-Fi connection."""
|
||||
if not self._manager:
|
||||
return
|
||||
self._run_coroutine(self._manager.activate_connection(ssid))
|
||||
|
||||
def connect_to_network(self, ssid: str, password: str = None, bssid: str = None, is_hidden: bool = False):
|
||||
"""Connect to a Wi-Fi network."""
|
||||
if not self._manager:
|
||||
return
|
||||
self._run_coroutine(self._manager.connect_to_network(ssid, password, bssid, is_hidden))
|
||||
|
||||
def _run_coroutine(self, coro):
|
||||
"""Run a coroutine in the async thread."""
|
||||
if not self._running or not self._loop:
|
||||
cloudlog.error("WifiManager thread is not running")
|
||||
return
|
||||
asyncio.run_coroutine_threadsafe(coro, self._loop)
|
||||
58
system/ui/lib/window.py
Normal file
58
system/ui/lib/window.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
from typing import Generic, Protocol, TypeVar
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
|
||||
|
||||
class RendererProtocol(Protocol):
|
||||
def render(self): ...
|
||||
|
||||
|
||||
R = TypeVar("R", bound=RendererProtocol)
|
||||
|
||||
|
||||
class BaseWindow(Generic[R]):
|
||||
def __init__(self, title: str):
|
||||
self._title = title
|
||||
self._renderer: R | None = None
|
||||
self._stop_event = threading.Event()
|
||||
self._thread = threading.Thread(target=self._run)
|
||||
self._thread.start()
|
||||
|
||||
# wait for the renderer to be initialized
|
||||
while self._renderer is None and self._thread.is_alive():
|
||||
time.sleep(0.01)
|
||||
|
||||
def _create_renderer(self) -> R:
|
||||
raise NotImplementedError()
|
||||
|
||||
def _run(self):
|
||||
if os.getenv("CI") is not None:
|
||||
return
|
||||
gui_app.init_window(self._title)
|
||||
self._renderer = self._create_renderer()
|
||||
try:
|
||||
for _ in gui_app.render():
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
self._renderer.render()
|
||||
finally:
|
||||
gui_app.close()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def close(self):
|
||||
if self._thread.is_alive():
|
||||
self._stop_event.set()
|
||||
self._thread.join(timeout=2.0)
|
||||
if self._thread.is_alive():
|
||||
cloudlog.warning(f"Failed to join {self._title} thread")
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
@@ -6,6 +6,7 @@ import time
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.window import BaseWindow
|
||||
from openpilot.system.ui.text import wrap_text
|
||||
|
||||
# Constants
|
||||
@@ -85,16 +86,12 @@ class SpinnerRenderer:
|
||||
FONT_SIZE, 0.0, rl.WHITE)
|
||||
|
||||
|
||||
class Spinner:
|
||||
class Spinner(BaseWindow[SpinnerRenderer]):
|
||||
def __init__(self):
|
||||
self._renderer: SpinnerRenderer | None = None
|
||||
self._stop_event = threading.Event()
|
||||
self._thread = threading.Thread(target=self._run)
|
||||
self._thread.start()
|
||||
super().__init__("Spinner")
|
||||
|
||||
# wait for the renderer to be initialized
|
||||
while self._renderer is None and self._thread.is_alive():
|
||||
time.sleep(0.01)
|
||||
def _create_renderer(self):
|
||||
return SpinnerRenderer()
|
||||
|
||||
def update(self, spinner_text: str):
|
||||
if self._renderer is not None:
|
||||
@@ -103,35 +100,6 @@ class Spinner:
|
||||
def update_progress(self, cur: float, total: float):
|
||||
self.update(str(round(100 * cur / total)))
|
||||
|
||||
def _run(self):
|
||||
if os.getenv("CI") is not None:
|
||||
return
|
||||
gui_app.init_window("Spinner")
|
||||
self._renderer = renderer = SpinnerRenderer()
|
||||
try:
|
||||
for _ in gui_app.render():
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
renderer.render()
|
||||
finally:
|
||||
gui_app.close()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def close(self):
|
||||
if self._thread.is_alive():
|
||||
self._stop_event.set()
|
||||
self._thread.join(timeout=2.0)
|
||||
if self._thread.is_alive():
|
||||
print("WARNING: failed to join spinner thread")
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with Spinner() as s:
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
import pyray as rl
|
||||
from openpilot.system.hardware import HARDWARE, PC
|
||||
from openpilot.system.ui.lib.button import gui_button, ButtonStyle
|
||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.window import BaseWindow
|
||||
|
||||
MARGIN = 50
|
||||
SPACING = 40
|
||||
@@ -74,52 +73,18 @@ class TextWindowRenderer:
|
||||
return ret
|
||||
|
||||
|
||||
class TextWindow:
|
||||
class TextWindow(BaseWindow[TextWindowRenderer]):
|
||||
def __init__(self, text: str):
|
||||
self._text = text
|
||||
super().__init__("Text")
|
||||
|
||||
self._renderer: TextWindowRenderer | None = None
|
||||
self._stop_event = threading.Event()
|
||||
self._thread = threading.Thread(target=self._run)
|
||||
self._thread.start()
|
||||
|
||||
# wait for the renderer to be initialized
|
||||
while self._renderer is None and self._thread.is_alive():
|
||||
time.sleep(0.01)
|
||||
def _create_renderer(self):
|
||||
return TextWindowRenderer(self._text)
|
||||
|
||||
def wait_for_exit(self):
|
||||
while self._thread.is_alive():
|
||||
time.sleep(0.01)
|
||||
|
||||
def _run(self):
|
||||
if os.getenv("CI") is not None:
|
||||
return
|
||||
gui_app.init_window("Text")
|
||||
self._renderer = renderer = TextWindowRenderer(self._text)
|
||||
try:
|
||||
for _ in gui_app.render():
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
renderer.render()
|
||||
finally:
|
||||
gui_app.close()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def close(self):
|
||||
if self._thread.is_alive():
|
||||
self._stop_event.set()
|
||||
self._thread.join(timeout=2.0)
|
||||
if self._thread.is_alive():
|
||||
print("WARNING: failed to join text window thread")
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with TextWindow(DEMO_TEXT):
|
||||
|
||||
@@ -44,25 +44,28 @@ keyboard_layouts = {
|
||||
class Keyboard:
|
||||
def __init__(self, max_text_size: int = 255):
|
||||
self._layout = keyboard_layouts["lowercase"]
|
||||
self._input_text = ""
|
||||
self._max_text_size = max_text_size
|
||||
self._string_pointer = rl.ffi.new("char[]", max_text_size)
|
||||
self._input_text = ""
|
||||
self._clear()
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
return self._input_text
|
||||
|
||||
def clear(self):
|
||||
self._input_text = ""
|
||||
def text(self):
|
||||
result = rl.ffi.string(self._string_pointer).decode("utf-8")
|
||||
self._clear()
|
||||
return result
|
||||
|
||||
def render(self, rect, title, sub_title):
|
||||
gui_label(rl.Rectangle(rect.x, rect.y, rect.width, 95), title, 90)
|
||||
gui_label(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60), sub_title, 55, rl.GRAY)
|
||||
if gui_button(rl.Rectangle(rect.x + rect.width - 300, rect.y, 300, 100), "Cancel"):
|
||||
return -1
|
||||
self._clear()
|
||||
return 0
|
||||
|
||||
# Text box for input
|
||||
rl.gui_text_box(rl.Rectangle(rect.x, rect.y + 160, rect.width, 100), self._input_text, self._max_text_size, True)
|
||||
|
||||
self._sync_string_pointer()
|
||||
rl.gui_text_box(rl.Rectangle(rect.x, rect.y + 160, rect.width, 100), self._string_pointer, self._max_text_size, True)
|
||||
self._input_text = rl.ffi.string(self._string_pointer).decode("utf-8")
|
||||
h_space, v_space = 15, 15
|
||||
row_y_start = rect.y + 300 # Starting Y position for the first row
|
||||
key_height = (rect.height - 300 - 3 * v_space) / 4
|
||||
@@ -87,7 +90,7 @@ class Keyboard:
|
||||
else:
|
||||
self.handle_key_press(key)
|
||||
|
||||
return 0
|
||||
return -1
|
||||
|
||||
def handle_key_press(self, key):
|
||||
if key in (SHIFT_DOWN_KEY, ABC_KEY):
|
||||
@@ -102,3 +105,14 @@ class Keyboard:
|
||||
self._input_text = self._input_text[:-1]
|
||||
elif key != BACKSPACE_KEY and len(self._input_text) < self._max_text_size:
|
||||
self._input_text += key
|
||||
|
||||
def _clear(self):
|
||||
self._input_text = ''
|
||||
self._string_pointer[0] = b'\0'
|
||||
|
||||
def _sync_string_pointer(self):
|
||||
"""Sync the C-string pointer with the internal Python string."""
|
||||
encoded = self._input_text.encode("utf-8")[:self._max_text_size - 1] # Leave room for the null terminator
|
||||
buffer = rl.ffi.buffer(self._string_pointer)
|
||||
buffer[:len(encoded)] = encoded
|
||||
self._string_pointer[len(encoded)] = b'\0' # Null terminator
|
||||
|
||||
172
system/ui/widgets/network.py
Normal file
172
system/ui/widgets/network.py
Normal file
@@ -0,0 +1,172 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.lib.wifi_manager import NetworkInfo, WifiManagerCallbacks, WifiManagerWrapper
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.button import gui_button
|
||||
from openpilot.system.ui.lib.label import gui_label
|
||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||
from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog
|
||||
|
||||
NM_DEVICE_STATE_NEED_AUTH = 60
|
||||
ITEM_HEIGHT = 160
|
||||
|
||||
|
||||
@dataclass
|
||||
class StateIdle:
|
||||
action: Literal["idle"] = "idle"
|
||||
|
||||
@dataclass
|
||||
class StateConnecting:
|
||||
network: NetworkInfo
|
||||
action: Literal["connecting"] = "connecting"
|
||||
|
||||
@dataclass
|
||||
class StateNeedsAuth:
|
||||
network: NetworkInfo
|
||||
action: Literal["needs_auth"] = "needs_auth"
|
||||
|
||||
@dataclass
|
||||
class StateShowForgetConfirm:
|
||||
network: NetworkInfo
|
||||
action: Literal["show_forget_confirm"] = "show_forget_confirm"
|
||||
|
||||
@dataclass
|
||||
class StateForgetting:
|
||||
network: NetworkInfo
|
||||
action: Literal["forgetting"] = "forgetting"
|
||||
|
||||
UIState = StateIdle | StateConnecting | StateNeedsAuth | StateShowForgetConfirm | StateForgetting
|
||||
|
||||
|
||||
class WifiManagerUI:
|
||||
def __init__(self, wifi_manager: WifiManagerWrapper):
|
||||
self.state: UIState = StateIdle()
|
||||
self.btn_width = 200
|
||||
self.scroll_panel = GuiScrollPanel()
|
||||
self.keyboard = Keyboard()
|
||||
|
||||
self.wifi_manager = wifi_manager
|
||||
self.wifi_manager.set_callbacks(WifiManagerCallbacks(self._on_need_auth, self._on_activated, self._on_forgotten))
|
||||
self.wifi_manager.start()
|
||||
self.wifi_manager.connect()
|
||||
|
||||
def render(self, rect: rl.Rectangle):
|
||||
if not self.wifi_manager.networks:
|
||||
gui_label(rect, "Scanning Wi-Fi networks...", 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
return
|
||||
|
||||
match self.state:
|
||||
case StateNeedsAuth(network):
|
||||
result = self.keyboard.render(rect, "Enter password", f"for {network.ssid}")
|
||||
if result == 1:
|
||||
self.connect_to_network(network, self.keyboard.text)
|
||||
elif result == 0:
|
||||
self.state = StateIdle()
|
||||
|
||||
case StateShowForgetConfirm(network):
|
||||
result = confirm_dialog(rect, f'Forget Wi-Fi Network "{network.ssid}"?', "Forget")
|
||||
if result == 1:
|
||||
self.forget_network(network)
|
||||
elif result == 0:
|
||||
self.state = StateIdle()
|
||||
|
||||
case _:
|
||||
self._draw_network_list(rect)
|
||||
|
||||
def _draw_network_list(self, rect: rl.Rectangle):
|
||||
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, len(self.wifi_manager.networks) * ITEM_HEIGHT)
|
||||
offset = self.scroll_panel.handle_scroll(rect, content_rect)
|
||||
clicked = self.scroll_panel.is_click_valid()
|
||||
|
||||
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
||||
for i, network in enumerate(self.wifi_manager.networks):
|
||||
y_offset = rect.y + i * ITEM_HEIGHT + offset.y
|
||||
item_rect = rl.Rectangle(rect.x, y_offset, rect.width, ITEM_HEIGHT)
|
||||
if not rl.check_collision_recs(item_rect, rect):
|
||||
continue
|
||||
|
||||
self._draw_network_item(item_rect, network, clicked)
|
||||
if i < len(self.wifi_manager.networks) - 1:
|
||||
line_y = int(item_rect.y + item_rect.height - 1)
|
||||
rl.draw_line(int(item_rect.x), int(line_y), int(item_rect.x + item_rect.width), line_y, rl.LIGHTGRAY)
|
||||
|
||||
rl.end_scissor_mode()
|
||||
|
||||
def _draw_network_item(self, rect, network: NetworkInfo, clicked: bool):
|
||||
label_rect = rl.Rectangle(rect.x, rect.y, rect.width - self.btn_width * 2, ITEM_HEIGHT)
|
||||
state_rect = rl.Rectangle(rect.x + rect.width - self.btn_width * 2 - 150, rect.y, 300, ITEM_HEIGHT)
|
||||
|
||||
gui_label(label_rect, network.ssid, 55)
|
||||
|
||||
status_text = ""
|
||||
if network.is_connected:
|
||||
status_text = "Connected"
|
||||
match self.state:
|
||||
case StateConnecting(network=connecting):
|
||||
if connecting.ssid == network.ssid:
|
||||
status_text = "CONNECTING..."
|
||||
case StateForgetting(network=forgetting):
|
||||
if forgetting.ssid == network.ssid:
|
||||
status_text = "FORGETTING..."
|
||||
if status_text:
|
||||
rl.gui_label(state_rect, status_text)
|
||||
|
||||
# If the network is saved, show the "Forget" button
|
||||
if self.wifi_manager.is_saved(network.ssid):
|
||||
forget_btn_rect = rl.Rectangle(
|
||||
rect.x + rect.width - self.btn_width,
|
||||
rect.y + (ITEM_HEIGHT - 80) / 2,
|
||||
self.btn_width,
|
||||
80,
|
||||
)
|
||||
if isinstance(self.state, StateIdle) and gui_button(forget_btn_rect, "Forget") and clicked:
|
||||
self.state = StateShowForgetConfirm(network)
|
||||
|
||||
if isinstance(self.state, StateIdle) and rl.check_collision_point_rec(rl.get_mouse_position(), label_rect) and clicked:
|
||||
if not self.wifi_manager.is_saved(network.ssid):
|
||||
self.state = StateNeedsAuth(network)
|
||||
else:
|
||||
self.connect_to_network(network)
|
||||
|
||||
def connect_to_network(self, network: NetworkInfo, password=''):
|
||||
self.state = StateConnecting(network)
|
||||
if self.wifi_manager.is_saved(network.ssid) and not password:
|
||||
self.wifi_manager.activate_connection(network.ssid)
|
||||
else:
|
||||
self.wifi_manager.connect_to_network(network.ssid, password)
|
||||
|
||||
def forget_network(self, network: NetworkInfo):
|
||||
self.state = StateForgetting(network)
|
||||
self.wifi_manager.forget_connection(network.ssid)
|
||||
|
||||
def _on_need_auth(self):
|
||||
match self.state:
|
||||
case StateConnecting(network):
|
||||
self.state = StateNeedsAuth(network)
|
||||
|
||||
def _on_activated(self):
|
||||
if isinstance(self.state, StateConnecting):
|
||||
self.state = StateIdle()
|
||||
|
||||
def _on_forgotten(self):
|
||||
if isinstance(self.state, StateForgetting):
|
||||
self.state = StateIdle()
|
||||
|
||||
|
||||
def main():
|
||||
gui_app.init_window("Wi-Fi Manager")
|
||||
wifi_manager = WifiManagerWrapper()
|
||||
wifi_ui = WifiManagerUI(wifi_manager)
|
||||
|
||||
for _ in gui_app.render():
|
||||
wifi_ui.render(rl.Rectangle(50, 50, gui_app.width - 100, gui_app.height - 100))
|
||||
|
||||
wifi_manager.shutdown()
|
||||
gui_app.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env expect
|
||||
spawn adb shell
|
||||
expect "#"
|
||||
send "cd data/openpilot\r"
|
||||
send "export TERM=xterm-256color\r"
|
||||
send "su comma\r"
|
||||
send "clear\r"
|
||||
interact
|
||||
17
tools/auto_source.py
Executable file
17
tools/auto_source.py
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from openpilot.tools.lib.logreader import LogReader
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python auto_source.py <log_path>")
|
||||
sys.exit(1)
|
||||
|
||||
log_path = sys.argv[1]
|
||||
lr = LogReader(log_path, sort_by_time=True)
|
||||
print("\n".join(lr.logreader_identifiers))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -12,6 +12,8 @@ Options:
|
||||
-h, --help Displays help on commandline options.
|
||||
--help-all Displays help including Qt specific options.
|
||||
--demo use a demo route instead of providing your own
|
||||
--auto Auto load the route from the best available source (no video):
|
||||
internal, openpilotci, comma_api, car_segments, testing_closet
|
||||
--qcam load qcamera
|
||||
--ecam load wide road camera
|
||||
--msgq read can messages from msgq
|
||||
|
||||
@@ -23,6 +23,7 @@ int main(int argc, char *argv[]) {
|
||||
cmd_parser.addHelpOption();
|
||||
cmd_parser.addPositionalArgument("route", "the drive to replay. find your drives at connect.comma.ai");
|
||||
cmd_parser.addOption({"demo", "use a demo route instead of providing your own"});
|
||||
cmd_parser.addOption({"auto", "Auto load the route from the best available source (no video): internal, openpilotci, comma_api, car_segments, testing_closet"});
|
||||
cmd_parser.addOption({"qcam", "load qcamera"});
|
||||
cmd_parser.addOption({"ecam", "load wide road camera"});
|
||||
cmd_parser.addOption({"dcam", "load driver camera"});
|
||||
@@ -69,7 +70,8 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
if (!route.isEmpty()) {
|
||||
auto replay_stream = std::make_unique<ReplayStream>(&app);
|
||||
if (!replay_stream->loadRoute(route, cmd_parser.value("data_dir"), replay_flags)) {
|
||||
bool auto_source = cmd_parser.isSet("auto");
|
||||
if (!replay_stream->loadRoute(route, cmd_parser.value("data_dir"), replay_flags, auto_source)) {
|
||||
return 0;
|
||||
}
|
||||
stream = replay_stream.release();
|
||||
|
||||
@@ -180,25 +180,36 @@ bool cabana::Signal::operator==(const cabana::Signal &other) const {
|
||||
// helper functions
|
||||
|
||||
double get_raw_value(const uint8_t *data, size_t data_size, const cabana::Signal &sig) {
|
||||
int64_t val = 0;
|
||||
const int msb_byte = sig.msb / 8;
|
||||
if (msb_byte >= (int)data_size) return 0;
|
||||
|
||||
int i = sig.msb / 8;
|
||||
int bits = sig.size;
|
||||
while (i >= 0 && i < data_size && bits > 0) {
|
||||
int lsb = (int)(sig.lsb / 8) == i ? sig.lsb : i * 8;
|
||||
int msb = (int)(sig.msb / 8) == i ? sig.msb : (i + 1) * 8 - 1;
|
||||
int size = msb - lsb + 1;
|
||||
const int lsb_byte = sig.lsb / 8;
|
||||
uint64_t val = 0;
|
||||
|
||||
uint64_t d = (data[i] >> (lsb - (i * 8))) & ((1ULL << size) - 1);
|
||||
val |= d << (bits - size);
|
||||
|
||||
bits -= size;
|
||||
i = sig.is_little_endian ? i - 1 : i + 1;
|
||||
// Fast path: signal fits in a single byte
|
||||
if (msb_byte == lsb_byte) {
|
||||
val = (data[msb_byte] >> (sig.lsb & 7)) & ((1ULL << sig.size) - 1);
|
||||
} else {
|
||||
// Multi-byte case: signal spans across multiple bytes
|
||||
int bits = sig.size;
|
||||
int i = msb_byte;
|
||||
const int step = sig.is_little_endian ? -1 : 1;
|
||||
while (i >= 0 && i < (int)data_size && bits > 0) {
|
||||
const int msb = (i == msb_byte) ? sig.msb & 7 : 7;
|
||||
const int lsb = (i == lsb_byte) ? sig.lsb & 7 : 0;
|
||||
const int nbits = msb - lsb + 1;
|
||||
val = (val << nbits) | ((data[i] >> lsb) & ((1ULL << nbits) - 1));
|
||||
bits -= nbits;
|
||||
i += step;
|
||||
}
|
||||
}
|
||||
if (sig.is_signed) {
|
||||
val -= ((val >> (sig.size - 1)) & 0x1) ? (1ULL << sig.size) : 0;
|
||||
|
||||
// Sign extension (if needed)
|
||||
if (sig.is_signed && (val & (1ULL << (sig.size - 1)))) {
|
||||
val |= ~((1ULL << sig.size) - 1);
|
||||
}
|
||||
return val * sig.factor + sig.offset;
|
||||
|
||||
return static_cast<int64_t>(val) * sig.factor + sig.offset;
|
||||
}
|
||||
|
||||
void updateMsbLsb(cabana::Signal &s) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QPushButton>
|
||||
|
||||
#include "common/timing.h"
|
||||
#include "common/util.h"
|
||||
#include "tools/cabana/streams/routes.h"
|
||||
|
||||
ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent) {
|
||||
@@ -45,9 +46,9 @@ void ReplayStream::mergeSegments() {
|
||||
}
|
||||
}
|
||||
|
||||
bool ReplayStream::loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags) {
|
||||
bool ReplayStream::loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags, bool auto_source) {
|
||||
replay.reset(new Replay(route.toStdString(), {"can", "roadEncodeIdx", "driverEncodeIdx", "wideRoadEncodeIdx", "carParams"},
|
||||
{}, nullptr, replay_flags, data_dir.toStdString()));
|
||||
{}, nullptr, replay_flags, data_dir.toStdString(), auto_source));
|
||||
replay->setSegmentCacheLimit(settings.max_cached_minutes);
|
||||
replay->installEventFilter([this](const Event *event) { return eventFilter(event); });
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class ReplayStream : public AbstractStream {
|
||||
public:
|
||||
ReplayStream(QObject *parent);
|
||||
void start() override { replay->start(); }
|
||||
bool loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE);
|
||||
bool loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE, bool auto_source = false);
|
||||
bool eventFilter(const Event *event);
|
||||
void seekTo(double ts) override { replay->seekTo(std::max(double(0), ts), false); }
|
||||
bool liveStreaming() const override { return false; }
|
||||
|
||||
@@ -16,7 +16,7 @@ from typing import Literal
|
||||
from cereal.messaging import SubMaster
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.tools.lib.route import SegmentRange, get_max_seg_number_cached
|
||||
from openpilot.tools.lib.route import Route
|
||||
|
||||
DEFAULT_OUTPUT = 'output.mp4'
|
||||
DEMO_START = 90
|
||||
@@ -26,7 +26,7 @@ FRAMERATE = 20
|
||||
PIXEL_DEPTH = '24'
|
||||
RESOLUTION = '2160x1080'
|
||||
SECONDS_TO_WARM = 2
|
||||
PROC_WAIT_SECONDS = 5
|
||||
PROC_WAIT_SECONDS = 30
|
||||
|
||||
REPLAY = str(Path(BASEDIR, 'tools/replay/replay').resolve())
|
||||
UI = str(Path(BASEDIR, 'selfdrive/ui/ui').resolve())
|
||||
@@ -52,6 +52,29 @@ def check_for_failure(proc: Popen):
|
||||
raise ChildProcessError(msg)
|
||||
|
||||
|
||||
def escape_ffmpeg_text(value: str):
|
||||
special_chars = {',': '\\,', ':': '\\:', '=': '\\=', '[': '\\[', ']': '\\]'}
|
||||
value = value.replace('\\', '\\\\\\\\\\\\\\\\')
|
||||
for char, escaped in special_chars.items():
|
||||
value = value.replace(char, escaped)
|
||||
return value
|
||||
|
||||
|
||||
def get_meta_text(route: Route):
|
||||
metadata = route.get_metadata()
|
||||
origin_parts = metadata['git_remote'].split('/')
|
||||
origin = origin_parts[3] if len(origin_parts) > 3 else 'unknown'
|
||||
return ', '.join([
|
||||
f"openpilot v{metadata['version']}",
|
||||
f"route: {metadata['fullname']}",
|
||||
f"car: {metadata['platform']}",
|
||||
f"origin: {origin}",
|
||||
f"branch: {metadata['git_branch']}",
|
||||
f"commit: {metadata['git_commit'][:7]}",
|
||||
f"modified: {str(metadata['git_dirty']).lower()}",
|
||||
])
|
||||
|
||||
|
||||
def parse_args(parser: ArgumentParser):
|
||||
args = parser.parse_args()
|
||||
if args.demo:
|
||||
@@ -74,20 +97,17 @@ def parse_args(parser: ArgumentParser):
|
||||
if args.start < SECONDS_TO_WARM:
|
||||
parser.error(f'start must be greater than {SECONDS_TO_WARM}s to allow the UI time to warm up')
|
||||
|
||||
# if using local files, don't worry about length check right now so we skip the network call
|
||||
# TODO: derive segment count from local FS
|
||||
if not args.data_dir:
|
||||
try:
|
||||
num_segs = get_max_seg_number_cached(SegmentRange(args.route))
|
||||
except Exception as e:
|
||||
parser.error(f'failed to get route length: {e}')
|
||||
try:
|
||||
args.route = Route(args.route, data_dir=args.data_dir)
|
||||
except Exception as e:
|
||||
parser.error(f'failed to get route: {e}')
|
||||
|
||||
# FIXME: length isn't exactly max segment seconds, simplify to replay exiting at end of data
|
||||
length = round(num_segs * 60)
|
||||
if args.start >= length:
|
||||
parser.error(f'start ({args.start}s) cannot be after end of route ({length}s)')
|
||||
if args.end > length:
|
||||
parser.error(f'end ({args.end}s) cannot be after end of route ({length}s)')
|
||||
# FIXME: length isn't exactly max segment seconds, simplify to replay exiting at end of data
|
||||
length = round(args.route.max_seg_number * 60)
|
||||
if args.start >= length:
|
||||
parser.error(f'start ({args.start}s) cannot be after end of route ({length}s)')
|
||||
if args.end > length:
|
||||
parser.error(f'end ({args.end}s) cannot be after end of route ({length}s)')
|
||||
|
||||
return args
|
||||
|
||||
@@ -119,6 +139,12 @@ def validate_route(route: str):
|
||||
return route
|
||||
|
||||
|
||||
def validate_title(title: str):
|
||||
if len(title) > 80:
|
||||
raise ArgumentTypeError('title must be no longer than 80 chars')
|
||||
return title
|
||||
|
||||
|
||||
def wait_for_frames(procs: list[Popen]):
|
||||
sm = SubMaster(['uiDebug'])
|
||||
no_frames_drawn = True
|
||||
@@ -129,20 +155,27 @@ def wait_for_frames(procs: list[Popen]):
|
||||
check_for_failure(proc)
|
||||
|
||||
|
||||
def clip(data_dir: str | None, quality: Literal['low', 'high'], prefix: str, route: str, output_filepath: str, start: int, end: int, target_size_mb: int):
|
||||
logger.info(f'clipping route {route}, start={start} end={end} quality={quality} target_filesize={target_size_mb}MB')
|
||||
def clip(data_dir: str | None, quality: Literal['low', 'high'], prefix: str, route: Route, out: str, start: int, end: int, target_mb: int, title: str | None):
|
||||
logger.info(f'clipping route {route.name.canonical_name}, start={start} end={end} quality={quality} target_filesize={target_mb}MB')
|
||||
|
||||
begin_at = max(start - SECONDS_TO_WARM, 0)
|
||||
duration = end - start
|
||||
bit_rate_kbps = int(round(target_size_mb * 8 * 1024 * 1024 / duration / 1000))
|
||||
bit_rate_kbps = int(round(target_mb * 8 * 1024 * 1024 / duration / 1000))
|
||||
|
||||
# TODO: evaluate creating fn that inspects /tmp/.X11-unix and creates unused display to avoid possibility of collision
|
||||
display = f':{randint(99, 999)}'
|
||||
|
||||
meta_text = get_meta_text(route)
|
||||
overlays = [
|
||||
f"drawtext=text='{escape_ffmpeg_text(meta_text)}':fontfile=Inter.tff:fontcolor=white:fontsize=18:box=1:boxcolor=black@0.33:boxborderw=7:x=(w-text_w)/2:y=5.5:enable='between(t,1,5)'"
|
||||
]
|
||||
if title:
|
||||
overlays.append(f"drawtext=text='{escape_ffmpeg_text(title)}':fontfile=Inter.tff:fontcolor=white:fontsize=32:box=1:boxcolor=black@0.33:boxborderw=10:x=(w-text_w)/2:y=53")
|
||||
|
||||
ffmpeg_cmd = [
|
||||
'ffmpeg', '-y', '-video_size', RESOLUTION, '-framerate', str(FRAMERATE), '-f', 'x11grab', '-draw_mouse', '0',
|
||||
'-i', display, '-c:v', 'libx264', '-maxrate', f'{bit_rate_kbps}k', '-bufsize', f'{bit_rate_kbps*2}k', '-crf', '23',
|
||||
'-preset', 'ultrafast', '-pix_fmt', 'yuv420p', '-movflags', '+faststart', '-f', 'mp4', '-t', str(duration), output_filepath,
|
||||
'-filter:v', ','.join(overlays), '-preset', 'ultrafast', '-pix_fmt', 'yuv420p', '-movflags', '+faststart', '-f', 'mp4', '-t', str(duration), out
|
||||
]
|
||||
|
||||
replay_cmd = [REPLAY, '-c', '1', '-s', str(begin_at), '--prefix', prefix]
|
||||
@@ -150,7 +183,7 @@ def clip(data_dir: str | None, quality: Literal['low', 'high'], prefix: str, rou
|
||||
replay_cmd.extend(['--data_dir', data_dir])
|
||||
if quality == 'low':
|
||||
replay_cmd.append('--qcam')
|
||||
replay_cmd.append(route)
|
||||
replay_cmd.append(route.name.canonical_name)
|
||||
|
||||
ui_cmd = [UI, '-platform', 'xcb']
|
||||
xvfb_cmd = ['Xvfb', display, '-terminate', '-screen', '0', f'{RESOLUTION}x{PIXEL_DEPTH}']
|
||||
@@ -183,7 +216,7 @@ def clip(data_dir: str | None, quality: Literal['low', 'high'], prefix: str, rou
|
||||
ffmpeg_proc.wait(duration + PROC_WAIT_SECONDS)
|
||||
for proc in procs:
|
||||
check_for_failure(proc)
|
||||
logger.info(f'recording complete: {Path(output_filepath).resolve()}')
|
||||
logger.info(f'recording complete: {Path(out).resolve()}')
|
||||
|
||||
|
||||
def main():
|
||||
@@ -199,9 +232,10 @@ def main():
|
||||
p.add_argument('-p', '--prefix', help='openpilot prefix', default=f'clip_{randint(100, 99999)}')
|
||||
p.add_argument('-q', '--quality', help='quality of camera (low = qcam, high = hevc)', choices=['low', 'high'], default='high')
|
||||
p.add_argument('-s', '--start', help='start clipping at <start> seconds', type=int)
|
||||
p.add_argument('-t', '--title', help='overlay this title on the video (e.g. "Chill driving across the Golden Gate Bridge")', type=validate_title)
|
||||
args = parse_args(p)
|
||||
try:
|
||||
clip(args.data_dir, args.quality, args.prefix, args.route, args.output, args.start, args.end, args.file_size)
|
||||
clip(args.data_dir, args.quality, args.prefix, args.route, args.output, args.start, args.end, args.file_size, args.title)
|
||||
except KeyboardInterrupt as e:
|
||||
logger.exception('interrupted by user', exc_info=e)
|
||||
except Exception as e:
|
||||
|
||||
@@ -33,6 +33,7 @@ function install_ubuntu_common_requirements() {
|
||||
git \
|
||||
git-lfs \
|
||||
ffmpeg \
|
||||
fonts-inter \
|
||||
libavformat-dev \
|
||||
libavcodec-dev \
|
||||
libavdevice-dev \
|
||||
|
||||
@@ -21,6 +21,7 @@ class Route:
|
||||
def __init__(self, name, data_dir=None):
|
||||
self._name = RouteName(name)
|
||||
self.files = None
|
||||
self.metadata = None
|
||||
if data_dir is not None:
|
||||
self._segments = self._get_segments_local(data_dir)
|
||||
else:
|
||||
@@ -59,6 +60,12 @@ class Route:
|
||||
qcamera_path_by_seg_num = {s.name.segment_num: s.qcamera_path for s in self._segments}
|
||||
return [qcamera_path_by_seg_num.get(i, None) for i in range(self.max_seg_number + 1)]
|
||||
|
||||
def get_metadata(self):
|
||||
if not self.metadata:
|
||||
api = CommaApi(get_token())
|
||||
self.metadata = api.get('v1/route/' + self.name.canonical_name)
|
||||
return self.metadata
|
||||
|
||||
# TODO: refactor this, it's super repetitive
|
||||
def _get_segments_remote(self):
|
||||
api = CommaApi(get_token())
|
||||
|
||||
16
tools/op.sh
16
tools/op.sh
@@ -52,6 +52,7 @@ function op_run_command() {
|
||||
# be default, assume openpilot dir is in current directory
|
||||
OPENPILOT_ROOT=$(pwd)
|
||||
function op_get_openpilot_dir() {
|
||||
# First try traversing up the directory tree
|
||||
while [[ "$OPENPILOT_ROOT" != '/' ]];
|
||||
do
|
||||
if find "$OPENPILOT_ROOT/launch_openpilot.sh" -maxdepth 1 -mindepth 1 &> /dev/null; then
|
||||
@@ -59,6 +60,14 @@ function op_get_openpilot_dir() {
|
||||
fi
|
||||
OPENPILOT_ROOT="$(readlink -f "$OPENPILOT_ROOT/"..)"
|
||||
done
|
||||
|
||||
# Fallback to hardcoded directories if not found
|
||||
for dir in "$HOME/openpilot" "/data/openpilot"; do
|
||||
if [[ -f "$dir/launch_openpilot.sh" ]]; then
|
||||
OPENPILOT_ROOT="$dir"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
function op_install_post_commit() {
|
||||
@@ -275,7 +284,7 @@ function op_venv() {
|
||||
|
||||
function op_adb() {
|
||||
op_before_cmd
|
||||
op_run_command tools/adb_shell.sh
|
||||
op_run_command tools/scripts/adb_ssh.sh
|
||||
}
|
||||
|
||||
function op_check() {
|
||||
@@ -378,11 +387,6 @@ function op_default() {
|
||||
echo " op is only a wrapper for existing scripts, tools, and commands."
|
||||
echo " op will always show you what it will run on your system."
|
||||
echo ""
|
||||
echo " op will try to find your openpilot directory in the following order:"
|
||||
echo " 1: use the directory specified with the --dir option"
|
||||
echo " 2: use the current working directory"
|
||||
echo " 3: go up the file tree non-recursively"
|
||||
echo ""
|
||||
echo -e "${BOLD}${UNDERLINE}Usage:${NC} op [OPTIONS] <COMMAND>"
|
||||
echo ""
|
||||
echo -e "${BOLD}${UNDERLINE}Commands [System]:${NC}"
|
||||
|
||||
@@ -64,6 +64,8 @@ Options:
|
||||
-s, --start <seconds> start from <seconds>
|
||||
-x <speed> playback <speed>. between 0.2 - 3
|
||||
--demo use a demo route instead of providing your own
|
||||
--auto Auto load the route from the best available source (no video):
|
||||
internal, openpilotci, comma_api, car_segments, testing_closet
|
||||
--data_dir <data_dir> local directory with routes
|
||||
--prefix <prefix> set OPENPILOT_PREFIX
|
||||
--dcam load driver camera
|
||||
|
||||
@@ -19,6 +19,8 @@ Options:
|
||||
-s, --start Start from <seconds>
|
||||
-x, --playback Playback <speed>
|
||||
--demo Use a demo route instead of providing your own
|
||||
--auto Auto load the route from the best available source (no video):
|
||||
internal, openpilotci, comma_api, car_segments, testing_closet
|
||||
-d, --data_dir Local directory with routes
|
||||
-p, --prefix Set OPENPILOT_PREFIX
|
||||
--dcam Load driver camera
|
||||
@@ -39,6 +41,7 @@ struct ReplayConfig {
|
||||
std::string data_dir;
|
||||
std::string prefix;
|
||||
uint32_t flags = REPLAY_FLAG_NONE;
|
||||
bool auto_source = false;
|
||||
int start_seconds = 0;
|
||||
int cache_segments = -1;
|
||||
float playback_speed = -1;
|
||||
@@ -52,6 +55,7 @@ bool parseArgs(int argc, char *argv[], ReplayConfig &config) {
|
||||
{"start", required_argument, nullptr, 's'},
|
||||
{"playback", required_argument, nullptr, 'x'},
|
||||
{"demo", no_argument, nullptr, 0},
|
||||
{"auto", no_argument, nullptr, 0},
|
||||
{"data_dir", required_argument, nullptr, 'd'},
|
||||
{"prefix", required_argument, nullptr, 'p'},
|
||||
{"dcam", no_argument, nullptr, 0},
|
||||
@@ -94,11 +98,9 @@ bool parseArgs(int argc, char *argv[], ReplayConfig &config) {
|
||||
case 'p': config.prefix = optarg; break;
|
||||
case 0: {
|
||||
std::string name = cli_options[option_index].name;
|
||||
if (name == "demo") {
|
||||
config.route = DEMO_ROUTE;
|
||||
} else {
|
||||
config.flags |= flag_map.at(name);
|
||||
}
|
||||
if (name == "demo") config.route = DEMO_ROUTE;
|
||||
else if (name == "auto") config.auto_source = true;
|
||||
else config.flags |= flag_map.at(name);
|
||||
break;
|
||||
}
|
||||
case 'h': std::cout << helpText; return false;
|
||||
@@ -136,7 +138,7 @@ int main(int argc, char *argv[]) {
|
||||
op_prefix = std::make_unique<OpenpilotPrefix>(config.prefix);
|
||||
}
|
||||
|
||||
Replay replay(config.route, config.allow, config.block, nullptr, config.flags, config.data_dir);
|
||||
Replay replay(config.route, config.allow, config.block, nullptr, config.flags, config.data_dir, config.auto_source);
|
||||
if (config.cache_segments > 0) {
|
||||
replay.setSegmentCacheLimit(config.cache_segments);
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ void notifyEvent(Callback &callback, Args &&...args) {
|
||||
}
|
||||
|
||||
Replay::Replay(const std::string &route, std::vector<std::string> allow, std::vector<std::string> block,
|
||||
SubMaster *sm, uint32_t flags, const std::string &data_dir)
|
||||
: sm_(sm), flags_(flags), seg_mgr_(std::make_unique<SegmentManager>(route, flags, data_dir)) {
|
||||
SubMaster *sm, uint32_t flags, const std::string &data_dir, bool auto_source)
|
||||
: sm_(sm), flags_(flags), seg_mgr_(std::make_unique<SegmentManager>(route, flags, data_dir, auto_source)) {
|
||||
std::signal(SIGUSR1, interrupt_sleep_handler);
|
||||
|
||||
if (!(flags_ & REPLAY_FLAG_ALL_SERVICES)) {
|
||||
|
||||
@@ -29,7 +29,7 @@ enum REPLAY_FLAGS {
|
||||
class Replay {
|
||||
public:
|
||||
Replay(const std::string &route, std::vector<std::string> allow, std::vector<std::string> block, SubMaster *sm = nullptr,
|
||||
uint32_t flags = REPLAY_FLAG_NONE, const std::string &data_dir = "");
|
||||
uint32_t flags = REPLAY_FLAG_NONE, const std::string &data_dir = "", bool auto_source = false);
|
||||
~Replay();
|
||||
bool load();
|
||||
RouteLoadError lastRouteError() const { return route().lastError(); }
|
||||
|
||||
@@ -10,9 +10,8 @@
|
||||
#include "tools/replay/replay.h"
|
||||
#include "tools/replay/util.h"
|
||||
|
||||
Route::Route(const std::string &route, const std::string &data_dir) : data_dir_(data_dir) {
|
||||
route_ = parseRoute(route);
|
||||
}
|
||||
Route::Route(const std::string &route, const std::string &data_dir, bool auto_source)
|
||||
: route_string_(route), data_dir_(data_dir), auto_source_(auto_source) {}
|
||||
|
||||
RouteIdentifier Route::parseRoute(const std::string &str) {
|
||||
RouteIdentifier identifier = {};
|
||||
@@ -44,27 +43,62 @@ RouteIdentifier Route::parseRoute(const std::string &str) {
|
||||
}
|
||||
|
||||
bool Route::load() {
|
||||
err_ = RouteLoadError::None;
|
||||
route_ = parseRoute(route_string_);
|
||||
if (route_.str.empty() || (data_dir_.empty() && route_.dongle_id.empty())) {
|
||||
rInfo("invalid route format");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse the timestamp from the route identifier (only applicable for old route formats).
|
||||
struct tm tm_time = {0};
|
||||
strptime(route_.timestamp.c_str(), "%Y-%m-%d--%H-%M-%S", &tm_time);
|
||||
date_time_ = mktime(&tm_time);
|
||||
if (strptime(route_.timestamp.c_str(), "%Y-%m-%d--%H-%M-%S", &tm_time)) {
|
||||
date_time_ = mktime(&tm_time);
|
||||
}
|
||||
|
||||
bool ret = data_dir_.empty() ? loadFromServer() : loadFromLocal();
|
||||
if (ret) {
|
||||
if (route_.begin_segment == -1) route_.begin_segment = segments_.rbegin()->first;
|
||||
if (route_.end_segment == -1) route_.end_segment = segments_.rbegin()->first;
|
||||
for (auto it = segments_.begin(); it != segments_.end(); /**/) {
|
||||
if (it->first < route_.begin_segment || it->first > route_.end_segment) {
|
||||
it = segments_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
if (!loadSegments()) {
|
||||
rInfo("Failed to load segments");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Route::loadSegments() {
|
||||
if (!auto_source_) {
|
||||
bool ret = data_dir_.empty() ? loadFromServer() : loadFromLocal();
|
||||
if (ret) {
|
||||
// Trim segments
|
||||
if (route_.begin_segment > 0) {
|
||||
segments_.erase(segments_.begin(), segments_.lower_bound(route_.begin_segment));
|
||||
}
|
||||
if (route_.end_segment >= 0) {
|
||||
segments_.erase(segments_.upper_bound(route_.end_segment), segments_.end());
|
||||
}
|
||||
}
|
||||
return !segments_.empty();
|
||||
}
|
||||
return loadFromAutoSource();
|
||||
}
|
||||
|
||||
bool Route::loadFromAutoSource() {
|
||||
auto origin_prefix = getenv("OPENPILOT_PREFIX");
|
||||
if (origin_prefix) {
|
||||
setenv("OPENPILOT_PREFIX", "", 1);
|
||||
}
|
||||
auto cmd = util::string_format("../auto_source.py \"%s\"", route_string_.c_str());
|
||||
auto log_files = split(util::check_output(cmd), '\n');
|
||||
if (origin_prefix) {
|
||||
setenv("OPENPILOT_PREFIX", origin_prefix, 1);
|
||||
}
|
||||
|
||||
const static std::regex rx(R"(\/(\d+)\/)");
|
||||
for (int i = 0; i < log_files.size(); ++i) {
|
||||
int seg_num = i;
|
||||
std::smatch match;
|
||||
if (std::regex_search(log_files[i], match, rx)) {
|
||||
seg_num = std::stoi(match[1]);
|
||||
}
|
||||
addFileToSegment(seg_num, log_files[i]);
|
||||
}
|
||||
return !segments_.empty();
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ struct SegmentFile {
|
||||
|
||||
class Route {
|
||||
public:
|
||||
Route(const std::string &route, const std::string &data_dir = {});
|
||||
Route(const std::string &route, const std::string &data_dir = {}, bool auto_source = false);
|
||||
bool load();
|
||||
RouteLoadError lastError() const { return err_; }
|
||||
inline const std::string &name() const { return route_.str; }
|
||||
@@ -52,6 +52,8 @@ public:
|
||||
static RouteIdentifier parseRoute(const std::string &str);
|
||||
|
||||
protected:
|
||||
bool loadSegments();
|
||||
bool loadFromAutoSource();
|
||||
bool loadFromLocal();
|
||||
bool loadFromServer(int retries = 3);
|
||||
bool loadFromJson(const std::string &json);
|
||||
@@ -59,8 +61,10 @@ protected:
|
||||
RouteIdentifier route_ = {};
|
||||
std::string data_dir_;
|
||||
std::map<int, SegmentFile> segments_;
|
||||
std::time_t date_time_;
|
||||
std::time_t date_time_ = 0;
|
||||
RouteLoadError err_ = RouteLoadError::None;
|
||||
bool auto_source_ = false;
|
||||
std::string route_string_;
|
||||
};
|
||||
|
||||
class Segment {
|
||||
|
||||
@@ -20,8 +20,8 @@ public:
|
||||
bool isSegmentLoaded(int n) const { return segments.find(n) != segments.end(); }
|
||||
};
|
||||
|
||||
SegmentManager(const std::string &route_name, uint32_t flags, const std::string &data_dir = "")
|
||||
: flags_(flags), route_(route_name, data_dir), event_data_(std::make_shared<EventData>()) {}
|
||||
SegmentManager(const std::string &route_name, uint32_t flags, const std::string &data_dir = "", bool auto_source = false)
|
||||
: flags_(flags), route_(route_name, data_dir, auto_source), event_data_(std::make_shared<EventData>()) {}
|
||||
~SegmentManager();
|
||||
|
||||
bool load();
|
||||
|
||||
7
tools/scripts/adb_ssh.sh
Executable file
7
tools/scripts/adb_ssh.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# this is a little nicer than "adb shell" since
|
||||
# "adb shell" doesn't do full terminal emulation
|
||||
adb forward tcp:2222 tcp:22
|
||||
ssh comma@localhost -p 2222
|
||||
Reference in New Issue
Block a user