mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-07-25 11:52:20 +08:00
Merge branch 'master' into sync-priv-20231108
# Conflicts: # panda # selfdrive/car/subaru/carcontroller.py # selfdrive/car/subaru/interface.py # selfdrive/controls/lib/lateral_planner.py
This commit is contained in:
@@ -5,6 +5,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
|
||||
|
||||
@@ -41,7 +41,7 @@ repos:
|
||||
args: ['--explicit-package-bases']
|
||||
exclude: '^(third_party/)|(cereal/)|(opendbc/)|(panda/)|(laika/)|(laika_repo/)|(rednose/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(xx/)'
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.1.1
|
||||
rev: v0.1.4
|
||||
hooks:
|
||||
- id: ruff
|
||||
exclude: '^(third_party/)|(cereal/)|(rednose/)|(panda/)|(laika/)|(laika_repo/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)'
|
||||
@@ -79,13 +79,13 @@ repos:
|
||||
language: script
|
||||
pass_filenames: false
|
||||
- repo: https://github.com/python-poetry/poetry
|
||||
rev: '1.6.0'
|
||||
rev: '1.7.0'
|
||||
hooks:
|
||||
- id: poetry-check
|
||||
name: validate poetry lock
|
||||
args:
|
||||
- --lock
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.27.0
|
||||
rev: 0.27.1
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
|
||||
Vendored
+3
-1
@@ -64,7 +64,9 @@ def deviceStage(String stageName, String deviceType, List env, def steps) {
|
||||
docker.image('ghcr.io/commaai/alpine-ssh').inside('--user=root') {
|
||||
lock(resource: "", label: deviceType, inversePrecedence: true, variable: 'device_ip', quantity: 1) {
|
||||
timeout(time: 20, unit: 'MINUTES') {
|
||||
device(device_ip, "git checkout", extra + "\n" + readFile("selfdrive/test/setup_device_ci.sh"))
|
||||
retry (3) {
|
||||
device(device_ip, "git checkout", extra + "\n" + readFile("selfdrive/test/setup_device_ci.sh"))
|
||||
}
|
||||
steps.each { item ->
|
||||
device(device_ip, item[0], item[1])
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
Version 0.9.5 (2023-XX-XX)
|
||||
Version 0.9.5 (2023-11-16)
|
||||
========================
|
||||
* New driving model
|
||||
* Improved navigate on openpilot performance using navigation instructions as an additional model input
|
||||
* Do lateral planning inside the model
|
||||
* New vision transformer architecture
|
||||
* Cadillac Escalade ESV 2019 support thanks to twilsonco!
|
||||
* Hyundai Azera 2022 support thanks to sunnyhaibin!
|
||||
* Hyundai Azera Hybrid 2020 support thanks to chanhojung and haram-KONA!
|
||||
|
||||
@@ -26,6 +26,10 @@ AddOption('--ubsan',
|
||||
action='store_true',
|
||||
help='turn on UBSan')
|
||||
|
||||
AddOption('--coverage',
|
||||
action='store_true',
|
||||
help='build with test coverage options')
|
||||
|
||||
AddOption('--clazy',
|
||||
action='store_true',
|
||||
help='build with clazy')
|
||||
|
||||
+1
-1
Submodule cereal updated: 536c6b793f...fc1a20047f
+2
-1
@@ -7,4 +7,5 @@ coverage:
|
||||
patch: off
|
||||
|
||||
ignore:
|
||||
- "**/test_*.py"
|
||||
- "**/test_*.py"
|
||||
- "selfdrive/test/**"
|
||||
@@ -2,6 +2,7 @@ import os
|
||||
import pytest
|
||||
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.system.hardware import TICI
|
||||
|
||||
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
@@ -31,3 +32,10 @@ def openpilot_class_fixture():
|
||||
|
||||
os.environ.clear()
|
||||
os.environ.update(starting_env)
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
skipper = pytest.mark.skip(reason="Skipping tici test on PC")
|
||||
for item in items:
|
||||
if not TICI and "tici" in item.keywords:
|
||||
item.add_marker(skipper)
|
||||
+8
-8
@@ -21,10 +21,10 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Cadillac|Escalade 2017[<sup>4</sup>](#footnotes)|Driver Assist Package|openpilot|0 mph|7 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-II connector<br>- 1 comma 3X<br>- 2 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Cadillac&model=Escalade 2017">Buy Here</a></sub></details>||
|
||||
|Cadillac|Escalade ESV 2016[<sup>4</sup>](#footnotes)|Adaptive Cruise Control (ACC) & LKAS|openpilot|0 mph|7 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-II connector<br>- 1 comma 3X<br>- 2 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Cadillac&model=Escalade ESV 2016">Buy Here</a></sub></details>||
|
||||
|Cadillac|Escalade ESV 2019[<sup>4</sup>](#footnotes)|Adaptive Cruise Control (ACC) & LKAS|openpilot|0 mph|7 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-II connector<br>- 1 comma 3X<br>- 2 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Cadillac&model=Escalade ESV 2019">Buy Here</a></sub></details>||
|
||||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Bolt EV 2022-23">Buy Here</a></sub></details>||
|
||||
|Chevrolet|Silverado 1500 2020-21|Safety Package II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Silverado 1500 2020-21">Buy Here</a></sub></details>||
|
||||
|Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Trailblazer 2021-22">Buy Here</a></sub></details>||
|
||||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Bolt EV 2022-23">Buy Here</a></sub></details>||
|
||||
|Chevrolet|Silverado 1500 2020-21|Safety Package II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Silverado 1500 2020-21">Buy Here</a></sub></details>||
|
||||
|Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Trailblazer 2021-22">Buy Here</a></sub></details>||
|
||||
|Chevrolet|Volt 2017-18[<sup>4</sup>](#footnotes)|Adaptive Cruise Control (ACC)|openpilot|0 mph|7 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-II connector<br>- 1 comma 3X<br>- 2 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Volt 2017-18">Buy Here</a></sub></details>|<a href="https://youtu.be/QeMCN_4TFfQ" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<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=Chrysler&model=Pacifica 2017-18">Buy Here</a></sub></details>||
|
||||
|Chrysler|Pacifica 2019-20|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<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=Chrysler&model=Pacifica 2019-20">Buy Here</a></sub></details>||
|
||||
@@ -50,14 +50,14 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Genesis|GV70 (3.5T Trim) 2022-23[<sup>6</sup>](#footnotes)|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai M connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=GV70 (3.5T Trim) 2022-23">Buy Here</a></sub></details>||
|
||||
|Genesis|GV80 2023[<sup>6</sup>](#footnotes)|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai M connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=GV80 2023">Buy Here</a></sub></details>||
|
||||
|GMC|Acadia 2018[<sup>4</sup>](#footnotes)|Adaptive Cruise Control (ACC)|openpilot|0 mph|7 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-II connector<br>- 1 comma 3X<br>- 2 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=GMC&model=Acadia 2018">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=0ZN6DdsBUZo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<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=GMC&model=Sierra 1500 2020-21">Buy Here</a></sub></details>|<a href="https://youtu.be/5HbNoBLzRwE" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=GMC&model=Sierra 1500 2020-21">Buy Here</a></sub></details>|<a href="https://youtu.be/5HbNoBLzRwE" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Honda|Accord 2018-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Accord 2018-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=mrUwlj3Mi58" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Honda|Accord Hybrid 2018-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Accord Hybrid 2018-22">Buy Here</a></sub></details>||
|
||||
|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic 2016-18">Buy Here</a></sub></details>|<a href="https://youtu.be/-IkImTe1NYE" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Honda|Civic 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|2 mph[<sup>5</sup>](#footnotes)|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=4Iz1Mz5LGF8" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Honda|Civic 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic 2022">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Honda|Civic 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Honda|Civic Hatchback 2017-21|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback 2017-21">Buy Here</a></sub></details>||
|
||||
|Honda|Civic Hatchback 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback 2022">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Honda|Civic Hatchback 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Honda|CR-V 2015-16|Touring Trim|openpilot|25 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V 2015-16">Buy Here</a></sub></details>||
|
||||
|Honda|CR-V 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V 2017-22">Buy Here</a></sub></details>||
|
||||
|Honda|CR-V Hybrid 2017-19|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V Hybrid 2017-19">Buy Here</a></sub></details>||
|
||||
@@ -110,7 +110,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Hyundai|Tucson 2022[<sup>6</sup>](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Tucson 2022">Buy Here</a></sub></details>||
|
||||
|Hyundai|Tucson 2023[<sup>6</sup>](#footnotes)|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Tucson 2023">Buy Here</a></sub></details>||
|
||||
|Hyundai|Tucson Diesel 2019|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai L connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Tucson Diesel 2019">Buy Here</a></sub></details>||
|
||||
|Hyundai|Tucson Hybrid 2022-23[<sup>6</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Tucson Hybrid 2022-23">Buy Here</a></sub></details>||
|
||||
|Hyundai|Tucson Hybrid 2022-24[<sup>6</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Tucson Hybrid 2022-24">Buy Here</a></sub></details>||
|
||||
|Hyundai|Veloster 2019-20|Smart Cruise Control (SCC)|Stock|5 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Veloster 2019-20">Buy Here</a></sub></details>||
|
||||
|Jeep|Grand Cherokee 2016-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<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=Jeep&model=Grand Cherokee 2016-18">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=eLR9o2JkuRk" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Jeep|Grand Cherokee 2019-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<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=Jeep&model=Grand Cherokee 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=jBe4lWnRSu4" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|
||||
+1
-1
Submodule panda updated: 43ad78a7e7...416b028322
Generated
+530
-559
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -5,7 +5,8 @@ addopts = "--ignore=openpilot/ --ignore=cereal/ --ignore=opendbc/ --ignore=panda
|
||||
python_files = "test_*.py"
|
||||
#timeout = "30" # you get this long by default
|
||||
markers = [
|
||||
"slow: tests that take awhile to run and can be skipped with -m 'not slow'"
|
||||
"slow: tests that take awhile to run and can be skipped with -m 'not slow'",
|
||||
"tici: tests that are only meant to run on the C3/C3X",
|
||||
]
|
||||
testpaths = [
|
||||
"common",
|
||||
@@ -140,6 +141,7 @@ parameterized = "^0.8"
|
||||
pprofile = "*"
|
||||
pre-commit = "*"
|
||||
pygame = "*"
|
||||
pympler = "*"
|
||||
pyprof2calltree = "*"
|
||||
pytest = "*"
|
||||
pytest-cov = "*"
|
||||
|
||||
@@ -6,6 +6,7 @@ launch_openpilot.sh
|
||||
|
||||
Jenkinsfile
|
||||
SConstruct
|
||||
pyproject.toml
|
||||
|
||||
README.md
|
||||
RELEASES.md
|
||||
@@ -623,15 +624,15 @@ opendbc/tesla_can.dbc
|
||||
opendbc/tesla_radar.dbc
|
||||
opendbc/tesla_powertrain.dbc
|
||||
|
||||
tinygrad_repo/openpilot/compile.py
|
||||
tinygrad_repo/openpilot/compile2.py
|
||||
tinygrad_repo/extra/onnx.py
|
||||
tinygrad_repo/extra/onnx_ops.py
|
||||
tinygrad_repo/extra/thneed.py
|
||||
tinygrad_repo/extra/utils.py
|
||||
tinygrad_repo/tinygrad/codegen/kernel.py
|
||||
tinygrad_repo/tinygrad/codegen/linearizer.py
|
||||
tinygrad_repo/tinygrad/codegen/optimizer.py
|
||||
tinygrad_repo/tinygrad/features/image.py
|
||||
tinygrad_repo/tinygrad/features/search.py
|
||||
tinygrad_repo/tinygrad/nn/*
|
||||
tinygrad_repo/tinygrad/renderer/cstyle.py
|
||||
tinygrad_repo/tinygrad/renderer/opencl.py
|
||||
|
||||
@@ -20,7 +20,7 @@ from dataclasses import asdict, dataclass, replace
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from queue import Queue
|
||||
from typing import BinaryIO, Callable, Dict, List, Optional, Set, Union, cast
|
||||
from typing import Callable, Dict, List, Optional, Set, Union, cast
|
||||
|
||||
import requests
|
||||
from jsonrpc import JSONRPCResponseManager, dispatcher
|
||||
@@ -290,19 +290,15 @@ def _do_upload(upload_item: UploadItem, callback: Optional[Callable] = None) ->
|
||||
compress = True
|
||||
|
||||
with open(path, "rb") as f:
|
||||
data: BinaryIO
|
||||
content = f.read()
|
||||
if compress:
|
||||
cloudlog.event("athena.upload_handler.compress", fn=path, fn_orig=upload_item.path)
|
||||
compressed = bz2.compress(f.read())
|
||||
size = len(compressed)
|
||||
data = io.BytesIO(compressed)
|
||||
else:
|
||||
size = os.fstat(f.fileno()).st_size
|
||||
data = f
|
||||
content = bz2.compress(content)
|
||||
|
||||
with io.BytesIO(content) as data:
|
||||
return requests.put(upload_item.url,
|
||||
data=CallbackReader(data, callback, size) if callback else data,
|
||||
headers={**upload_item.headers, 'Content-Length': str(size)},
|
||||
data=CallbackReader(data, callback, len(content)) if callback else data,
|
||||
headers={**upload_item.headers, 'Content-Length': str(len(content))},
|
||||
timeout=30)
|
||||
|
||||
|
||||
|
||||
@@ -9,10 +9,11 @@ import queue
|
||||
import unittest
|
||||
from dataclasses import asdict, replace
|
||||
from datetime import datetime, timedelta
|
||||
from parameterized import parameterized
|
||||
from typing import Optional
|
||||
|
||||
from multiprocessing import Process
|
||||
from pathlib import Path
|
||||
from pympler.tracker import SummaryTracker
|
||||
from unittest import mock
|
||||
from websocket import ABNF
|
||||
from websocket._exceptions import WebSocketConnectionClosedException
|
||||
@@ -57,10 +58,11 @@ class TestAthenadMethods(unittest.TestCase):
|
||||
break
|
||||
|
||||
@staticmethod
|
||||
def _create_file(file: str, parent: Optional[str] = None) -> str:
|
||||
def _create_file(file: str, parent: Optional[str] = None, data: bytes = b'') -> str:
|
||||
fn = os.path.join(Paths.log_root() if parent is None else parent, file)
|
||||
os.makedirs(os.path.dirname(fn), exist_ok=True)
|
||||
Path(fn).touch()
|
||||
with open(fn, 'wb') as f:
|
||||
f.write(data)
|
||||
return fn
|
||||
|
||||
|
||||
@@ -137,19 +139,31 @@ class TestAthenadMethods(unittest.TestCase):
|
||||
if fn.endswith('.bz2'):
|
||||
self.assertEqual(athenad.strip_bz2_extension(fn), fn[:-4])
|
||||
|
||||
|
||||
@parameterized.expand([(True,), (False,)])
|
||||
@with_http_server
|
||||
def test_do_upload(self, host):
|
||||
fn = self._create_file('qlog.bz2')
|
||||
def test_do_upload(self, compress, host):
|
||||
# random bytes to ensure rather large object post-compression
|
||||
fn = self._create_file('qlog', data=os.urandom(10000 * 1024))
|
||||
|
||||
item = athenad.UploadItem(path=fn, url="http://localhost:1238", headers={}, created_at=int(time.time()*1000), id='')
|
||||
# warm up object tracker
|
||||
tracker = SummaryTracker()
|
||||
for _ in range(5):
|
||||
tracker.diff()
|
||||
|
||||
upload_fn = fn + ('.bz2' if compress else '')
|
||||
item = athenad.UploadItem(path=upload_fn, url="http://localhost:1238", headers={}, created_at=int(time.time()*1000), id='')
|
||||
with self.assertRaises(requests.exceptions.ConnectionError):
|
||||
athenad._do_upload(item)
|
||||
|
||||
item = athenad.UploadItem(path=fn, url=f"{host}/qlog.bz2", headers={}, created_at=int(time.time()*1000), id='')
|
||||
item = athenad.UploadItem(path=upload_fn, url=f"{host}/qlog.bz2", headers={}, created_at=int(time.time()*1000), id='')
|
||||
resp = athenad._do_upload(item)
|
||||
self.assertEqual(resp.status_code, 201)
|
||||
|
||||
# assert memory cleaned up
|
||||
for _type, num_objects, total_size in tracker.diff():
|
||||
with self.subTest(_type=_type):
|
||||
self.assertLess(total_size / 1024, 10, f'Object {_type} ({num_objects=}) grew larger than 10 kB while uploading file')
|
||||
|
||||
@with_http_server
|
||||
def test_uploadFileToUrl(self, host):
|
||||
fn = self._create_file('qlog.bz2')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import pytest
|
||||
import time
|
||||
import unittest
|
||||
|
||||
@@ -8,17 +9,16 @@ from cereal import log
|
||||
from openpilot.common.gpio import gpio_set, gpio_init
|
||||
from panda import Panda, PandaDFU, PandaProtocolMismatch
|
||||
from openpilot.selfdrive.manager.process_config import managed_processes
|
||||
from openpilot.system.hardware import HARDWARE, PC
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.hardware.tici.pins import GPIO
|
||||
|
||||
HERE = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestPandad(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
if PC:
|
||||
raise unittest.SkipTest("needs a panda")
|
||||
# ensure panda is up
|
||||
if len(Panda.list()) == 0:
|
||||
self._run_test(60)
|
||||
|
||||
@@ -114,7 +114,7 @@ class CarHarness(EnumBase):
|
||||
hyundai_r = BaseCarHarness("Hyundai R connector")
|
||||
custom = BaseCarHarness("Developer connector")
|
||||
obd_ii = BaseCarHarness("OBD-II connector", parts=[Cable.long_obdc_cable, Cable.long_obdc_cable], has_connector=False)
|
||||
gm = BaseCarHarness("GM connector")
|
||||
gm = BaseCarHarness("GM connector", parts=[Accessory.harness_box])
|
||||
nissan_a = BaseCarHarness("Nissan A connector", parts=[Accessory.harness_box, Cable.rj45_cable_7ft, Cable.long_obdc_cable, Cable.usbc_coupler])
|
||||
nissan_b = BaseCarHarness("Nissan B connector", parts=[Accessory.harness_box, Cable.rj45_cable_7ft, Cable.long_obdc_cable, Cable.usbc_coupler])
|
||||
mazda = BaseCarHarness("Mazda connector")
|
||||
|
||||
@@ -139,8 +139,8 @@ CAR_INFO: Dict[str, Optional[Union[HondaCarInfo, List[HondaCarInfo]]]] = {
|
||||
],
|
||||
CAR.CIVIC_BOSCH_DIESEL: None, # same platform
|
||||
CAR.CIVIC_2022: [
|
||||
HondaCarInfo("Honda Civic 2022", "All", video_link="https://youtu.be/ytiOT5lcp6Q"),
|
||||
HondaCarInfo("Honda Civic Hatchback 2022", "All", video_link="https://youtu.be/ytiOT5lcp6Q"),
|
||||
HondaCarInfo("Honda Civic 2022-23", "All", video_link="https://youtu.be/ytiOT5lcp6Q"),
|
||||
HondaCarInfo("Honda Civic Hatchback 2022-23", "All", video_link="https://youtu.be/ytiOT5lcp6Q"),
|
||||
],
|
||||
CAR.ACURA_ILX: HondaCarInfo("Acura ILX 2016-19", "AcuraWatch Plus", min_steer_speed=25. * CV.MPH_TO_MS),
|
||||
CAR.CRV: HondaCarInfo("Honda CR-V 2015-16", "Touring Trim", min_steer_speed=12. * CV.MPH_TO_MS),
|
||||
@@ -1545,6 +1545,7 @@ FW_VERSIONS = {
|
||||
b'77959-T47-A940\x00\x00',
|
||||
b'77959-T47-A950\x00\x00',
|
||||
b'77959-T20-M820\x00\x00',
|
||||
b'77959-T20-A980\x00\x00',
|
||||
],
|
||||
(Ecu.combinationMeter, 0x18DA60F1, None): [
|
||||
b'78108-T21-A220\x00\x00',
|
||||
@@ -1553,6 +1554,7 @@ FW_VERSIONS = {
|
||||
b'78108-T21-A230\x00\x00',
|
||||
b'78108-T22-A020\x00\x00',
|
||||
b'78108-T21-MB10\x00\x00',
|
||||
b'78108-T21-A740\x00\x00',
|
||||
],
|
||||
(Ecu.fwdRadar, 0x18dab0f1, None): [
|
||||
b'36161-T20-A070\x00\x00',
|
||||
@@ -1580,6 +1582,7 @@ FW_VERSIONS = {
|
||||
b'37805-64A-A540\x00\x00',
|
||||
b'37805-64A-A620\x00\x00',
|
||||
b'37805-64D-P510\x00\x00',
|
||||
b'37805-64S-AA10\x00\x00',
|
||||
],
|
||||
},
|
||||
CAR.CLARITY: {
|
||||
|
||||
@@ -234,7 +234,7 @@ CAR_INFO: Dict[str, Optional[Union[HyundaiCarInfo, List[HyundaiCarInfo]]]] = {
|
||||
HyundaiCarInfo("Hyundai Tucson 2022", car_parts=CarParts.common([CarHarness.hyundai_n])),
|
||||
HyundaiCarInfo("Hyundai Tucson 2023", "All", car_parts=CarParts.common([CarHarness.hyundai_n])),
|
||||
],
|
||||
CAR.TUCSON_HYBRID_4TH_GEN: HyundaiCarInfo("Hyundai Tucson Hybrid 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_n])),
|
||||
CAR.TUCSON_HYBRID_4TH_GEN: HyundaiCarInfo("Hyundai Tucson Hybrid 2022-24", "All", car_parts=CarParts.common([CarHarness.hyundai_n])),
|
||||
CAR.SANTA_CRUZ_1ST_GEN: HyundaiCarInfo("Hyundai Santa Cruz 2022-23", car_parts=CarParts.common([CarHarness.hyundai_n])),
|
||||
CAR.CUSTIN_1ST_GEN: HyundaiCarInfo("Hyundai Custin 2023", "All", car_parts=CarParts.common([CarHarness.hyundai_k])),
|
||||
|
||||
@@ -2088,6 +2088,7 @@ FW_VERSIONS = {
|
||||
b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.01 99211-N9100 14A',
|
||||
b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-N9250 14W',
|
||||
b'\xf1\x00NX4 FR_CMR AT EUR LHD 1.00 2.02 99211-N9000 14E',
|
||||
b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-N9260 14Y',
|
||||
],
|
||||
(Ecu.fwdRadar, 0x7d0, None): [
|
||||
b'\xf1\x00NX4__ 1.00 1.00 99110-N9100 ',
|
||||
|
||||
@@ -5,7 +5,8 @@ from openpilot.common.params import Params
|
||||
from opendbc.can.packer import CANPacker
|
||||
from openpilot.selfdrive.car import apply_driver_steer_torque_limits, common_fault_avoidance
|
||||
from openpilot.selfdrive.car.subaru import subarucan
|
||||
from openpilot.selfdrive.car.subaru.values import DBC, GLOBAL_GEN2, PREGLOBAL_CARS, HYBRID_CARS, STEER_RATE_LIMITED, CanBus, CarControllerParams, SubaruFlags, SubaruFlagsSP
|
||||
from openpilot.selfdrive.car.subaru.values import DBC, GLOBAL_ES_ADDR, GLOBAL_GEN2, PREGLOBAL_CARS, HYBRID_CARS, STEER_RATE_LIMITED, \
|
||||
CanBus, CarControllerParams, SubaruFlags, SubaruFlagsSP
|
||||
|
||||
# FIXME: These limits aren't exact. The real limit is more than likely over a larger time period and
|
||||
# involves the total steering angle change rather than rate, but these limits work well for now
|
||||
@@ -154,6 +155,21 @@ class CarController:
|
||||
bus = CanBus.alt if self.CP.carFingerprint in GLOBAL_GEN2 else CanBus.main
|
||||
can_sends.append(subarucan.create_es_distance(self.packer, CS.es_distance_msg["COUNTER"] + 1, CS.es_distance_msg, bus, pcm_cancel_cmd))
|
||||
|
||||
if self.CP.flags & SubaruFlags.DISABLE_EYESIGHT:
|
||||
# Tester present (keeps eyesight disabled)
|
||||
if self.frame % 100 == 0:
|
||||
can_sends.append([GLOBAL_ES_ADDR, 0, b"\x02\x3E\x80\x00\x00\x00\x00\x00", CanBus.camera])
|
||||
|
||||
# Create all of the other eyesight messages to keep the rest of the car happy when eyesight is disabled
|
||||
if self.frame % 5 == 0:
|
||||
can_sends.append(subarucan.create_es_highbeamassist(self.packer))
|
||||
|
||||
if self.frame % 10 == 0:
|
||||
can_sends.append(subarucan.create_es_static_1(self.packer))
|
||||
|
||||
if self.frame % 2 == 0:
|
||||
can_sends.append(subarucan.create_es_static_2(self.packer))
|
||||
|
||||
new_actuators = actuators.copy()
|
||||
new_actuators.steer = self.apply_steer_last / self.p.STEER_MAX
|
||||
new_actuators.steerOutputCan = self.apply_steer_last
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from cereal import car
|
||||
from panda import Panda
|
||||
from openpilot.selfdrive.car import get_safety_config, create_mads_event
|
||||
from openpilot.selfdrive.car.disable_ecu import disable_ecu
|
||||
from openpilot.selfdrive.car.interfaces import CarInterfaceBase
|
||||
from openpilot.selfdrive.car.subaru.values import CAR, LKAS_ANGLE, GLOBAL_GEN2, PREGLOBAL_CARS, HYBRID_CARS, SubaruFlags, SubaruFlagsSP
|
||||
from openpilot.selfdrive.car.subaru.values import CAR, GLOBAL_ES_ADDR, LKAS_ANGLE, GLOBAL_GEN2, PREGLOBAL_CARS, HYBRID_CARS, SubaruFlags, SubaruFlagsSP
|
||||
|
||||
ButtonType = car.CarState.ButtonEvent.Type
|
||||
EventName = car.CarEvent.EventName
|
||||
@@ -136,6 +137,9 @@ class CarInterface(CarInterfaceBase):
|
||||
#ret.experimentalLongitudinalAvailable = candidate not in (GLOBAL_GEN2 | PREGLOBAL_CARS | LKAS_ANGLE | HYBRID_CARS)
|
||||
ret.openpilotLongitudinalControl = experimental_long and ret.experimentalLongitudinalAvailable
|
||||
|
||||
if candidate in GLOBAL_GEN2 and ret.openpilotLongitudinalControl:
|
||||
ret.flags |= SubaruFlags.DISABLE_EYESIGHT
|
||||
|
||||
if ret.openpilotLongitudinalControl:
|
||||
ret.longitudinalTuning.kpBP = [0., 5., 35.]
|
||||
ret.longitudinalTuning.kpV = [0.8, 1.0, 1.5]
|
||||
@@ -207,5 +211,10 @@ class CarInterface(CarInterfaceBase):
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def init(CP, logcan, sendcan):
|
||||
if CP.flags & SubaruFlags.DISABLE_EYESIGHT:
|
||||
disable_ecu(logcan, sendcan, bus=2, addr=GLOBAL_ES_ADDR, com_cont_req=b'\x28\x03\x01')
|
||||
|
||||
def apply(self, c, now_nanos):
|
||||
return self.CC.update(c, self.CS, now_nanos)
|
||||
|
||||
@@ -248,6 +248,30 @@ def create_es_infotainment(packer, frame, es_infotainment_msg, visual_alert):
|
||||
return packer.make_can_msg("ES_Infotainment", CanBus.main, values)
|
||||
|
||||
|
||||
def create_es_highbeamassist(packer):
|
||||
values = {
|
||||
"HBA_Available": False,
|
||||
}
|
||||
|
||||
return packer.make_can_msg("ES_HighBeamAssist", CanBus.main, values)
|
||||
|
||||
|
||||
def create_es_static_1(packer):
|
||||
values = {
|
||||
"SET_3": 3,
|
||||
}
|
||||
|
||||
return packer.make_can_msg("ES_STATIC_1", CanBus.main, values)
|
||||
|
||||
|
||||
def create_es_static_2(packer):
|
||||
values = {
|
||||
"SET_3": 3,
|
||||
}
|
||||
|
||||
return packer.make_can_msg("ES_STATIC_2", CanBus.main, values)
|
||||
|
||||
|
||||
# *** Subaru Pre-global ***
|
||||
|
||||
def subaru_preglobal_checksum(packer, values, addr, checksum_byte=7):
|
||||
|
||||
@@ -46,18 +46,23 @@ class CarControllerParams:
|
||||
|
||||
RPM_INACTIVE = 600 # a good base rpm for zero acceleration
|
||||
|
||||
THROTTLE_LOOKUP_BP = [0, 1]
|
||||
THROTTLE_LOOKUP_BP = [0, 2]
|
||||
THROTTLE_LOOKUP_V = [THROTTLE_INACTIVE, THROTTLE_MAX]
|
||||
|
||||
RPM_LOOKUP_BP = [0, 1]
|
||||
RPM_LOOKUP_BP = [0, 2]
|
||||
RPM_LOOKUP_V = [RPM_INACTIVE, RPM_MAX]
|
||||
|
||||
BRAKE_LOOKUP_BP = [-1, 0]
|
||||
BRAKE_LOOKUP_BP = [-3.5, 0]
|
||||
BRAKE_LOOKUP_V = [BRAKE_MAX, BRAKE_MIN]
|
||||
|
||||
|
||||
class SubaruFlags(IntFlag):
|
||||
SEND_INFOTAINMENT = 1
|
||||
DISABLE_EYESIGHT = 2
|
||||
|
||||
|
||||
GLOBAL_ES_ADDR = 0x787
|
||||
GEN2_ES_BUTTONS_DID = b'\x11\x30'
|
||||
|
||||
|
||||
class SubaruFlagsSP(IntFlag):
|
||||
@@ -446,6 +451,7 @@ FW_VERSIONS = {
|
||||
(Ecu.abs, 0x7b0, None): [
|
||||
b'\x7d\x97\x14\x40',
|
||||
b'\xf1\x00\xbb\x0c\x04',
|
||||
b'm\x97\x14@',
|
||||
],
|
||||
(Ecu.eps, 0x746, None): [
|
||||
b'}\xc0\x10\x00',
|
||||
@@ -454,13 +460,15 @@ FW_VERSIONS = {
|
||||
(Ecu.fwdCamera, 0x787, None): [
|
||||
b'\x00\x00\x64\x35\x1f\x40\x20\x09',
|
||||
b'\x00\x00c\xe9\x1f@ \x03',
|
||||
b'\x00\x00d\xd3\x1f@ \t'
|
||||
b'\x00\x00d\xd3\x1f@ \t',
|
||||
b'\x00\x00c\xe9\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.engine, 0x7e0, None): [
|
||||
b'\xba"@p\a',
|
||||
b'\xa7)\xa0q\a',
|
||||
b'\xf1\x82\xa7)\xa0q\a',
|
||||
b'\xba"@@\a',
|
||||
b'\xa7"@p\x07',
|
||||
],
|
||||
(Ecu.transmission, 0x7e1, None): [
|
||||
b'\xdc\xf2\x60\x60\x00',
|
||||
@@ -469,6 +477,7 @@ FW_VERSIONS = {
|
||||
b'\xdc\xf2`\x81\000',
|
||||
b'\xdc\xf2`\x80\x00',
|
||||
b'\x1a\xf6F`\x00',
|
||||
b'\xda\xf2`\x80\x00',
|
||||
],
|
||||
},
|
||||
CAR.LEGACY_PREGLOBAL: {
|
||||
@@ -477,6 +486,7 @@ FW_VERSIONS = {
|
||||
b'[\xba\xc4\x03',
|
||||
b'{\x97D\x00',
|
||||
b'[\x97D\000',
|
||||
b'k\x9aD\x00',
|
||||
],
|
||||
(Ecu.eps, 0x746, None): [
|
||||
b'[\xb0\x00\x01',
|
||||
@@ -493,12 +503,14 @@ FW_VERSIONS = {
|
||||
b'\xa0+@p\x07',
|
||||
b'\xb4"@0\x07',
|
||||
b'\xa0"@q\a',
|
||||
b'\xab+@p\x07',
|
||||
],
|
||||
(Ecu.transmission, 0x7e1, None): [
|
||||
b'\xbe\xf2\x00p\x00',
|
||||
b'\xbf\xfb\xc0\x80\x00',
|
||||
b'\xbd\xf2\x00`\x00',
|
||||
b'\xbf\xf2\000\x80\000',
|
||||
b'\xbe\xfb\xc0p\x00',
|
||||
],
|
||||
},
|
||||
CAR.OUTBACK_PREGLOBAL: {
|
||||
@@ -542,6 +554,7 @@ FW_VERSIONS = {
|
||||
b'\xb4"@r\a',
|
||||
b'\xa0+@@\x07',
|
||||
b'\xa0\"@\x80\a',
|
||||
b'\xa0*@u\x07',
|
||||
],
|
||||
(Ecu.transmission, 0x7e1, None): [
|
||||
b'\xbd\xfb\xe0\x80\x00',
|
||||
@@ -635,6 +648,7 @@ FW_VERSIONS = {
|
||||
b'\xbc"`q\x07',
|
||||
b'\xe3,\xa0@\x07',
|
||||
b'\xbc,\xa0u\x07',
|
||||
b'\xde,\xa0@\x07',
|
||||
],
|
||||
(Ecu.transmission, 0x7e1, None): [
|
||||
b'\xa5\xfe\xf7@\x00',
|
||||
@@ -723,5 +737,5 @@ PREGLOBAL_CARS = {CAR.FORESTER_PREGLOBAL, CAR.LEGACY_PREGLOBAL, CAR.OUTBACK_PREG
|
||||
HYBRID_CARS = {CAR.CROSSTREK_HYBRID, CAR.FORESTER_HYBRID}
|
||||
|
||||
# Cars that temporarily fault when steering angle rate is greater than some threshold.
|
||||
# Appears to be all cars that started production after 2020
|
||||
STEER_RATE_LIMITED = GLOBAL_GEN2 | {CAR.IMPREZA_2020}
|
||||
# Appears to be all torque-based cars produced around 2019 - present
|
||||
STEER_RATE_LIMITED = GLOBAL_GEN2 | {CAR.IMPREZA_2020, CAR.FORESTER}
|
||||
|
||||
@@ -251,6 +251,7 @@ routes = [
|
||||
CarTestRoute("791340bc01ed993d|2019-03-10--16-28-08", SUBARU.IMPREZA),
|
||||
CarTestRoute("8bf7e79a3ce64055|2021-05-24--09-36-27", SUBARU.IMPREZA_2020),
|
||||
# CarTestRoute("8de015561e1ea4a0|2023-08-29--17-08-31", SUBARU.IMPREZA), # openpilot longitudinal
|
||||
# CarTestRoute("c3d1ccb52f5f9d65|2023-07-22--01-23-20", SUBARU.OUTBACK, segment=9), # gen2 longitudinal, eyesight disabled
|
||||
CarTestRoute("1bbe6bf2d62f58a8|2022-07-14--17-11-43", SUBARU.OUTBACK, segment=10),
|
||||
CarTestRoute("c56e69bbc74b8fad|2022-08-18--09-43-51", SUBARU.LEGACY, segment=3),
|
||||
CarTestRoute("f4e3a0c511a076f4|2022-08-04--16-16-48", SUBARU.CROSSTREK_HYBRID, segment=2),
|
||||
|
||||
@@ -1941,6 +1941,7 @@ FW_VERSIONS = {
|
||||
b'\x01896634D44000\x00\x00\x00\x00',
|
||||
b'\x018966348X0000\x00\x00\x00\x00',
|
||||
b'\x01896630ED5000\x00\x00\x00\x00',
|
||||
b'\x018966348R9200\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.engine, 0x7e0, None): [
|
||||
b'\x02348X4000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
@@ -1962,6 +1963,7 @@ FW_VERSIONS = {
|
||||
b'F152648D60\x00\x00\x00\x00\x00\x00',
|
||||
b'F152648811\x00\x00\x00\x00\x00\x00',
|
||||
b'F152648C80\x00\x00\x00\x00\x00\x00',
|
||||
b'F152648493\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.eps, 0x7a1, None): [
|
||||
b'8965B48261\x00\x00\x00\x00\x00\x00',
|
||||
|
||||
@@ -679,11 +679,18 @@ class Controls:
|
||||
else:
|
||||
lac_log = log.ControlsState.LateralDebugState.new_message()
|
||||
if self.sm.rcv_frame['testJoystick'] > 0:
|
||||
# reset joystick if it hasn't been received in a while
|
||||
should_reset_joystick = (self.sm.frame - self.sm.rcv_frame['testJoystick'])*DT_CTRL > 0.2
|
||||
if not should_reset_joystick:
|
||||
joystick_axes = self.sm['testJoystick'].axes
|
||||
else:
|
||||
joystick_axes = [0.0, 0.0]
|
||||
|
||||
if CC.longActive:
|
||||
actuators.accel = 4.0*clip(self.sm['testJoystick'].axes[0], -1, 1)
|
||||
actuators.accel = 4.0*clip(joystick_axes[0], -1, 1)
|
||||
|
||||
if CC.latActive:
|
||||
steer = clip(self.sm['testJoystick'].axes[1], -1, 1)
|
||||
steer = clip(joystick_axes[1], -1, 1)
|
||||
# max angle is 45 for angle-based cars, max curvature is 0.02
|
||||
actuators.steer, actuators.steeringAngleDeg, actuators.curvature = steer, steer * 45., steer * -0.02
|
||||
|
||||
|
||||
@@ -46,7 +46,8 @@ class LateralPlanner:
|
||||
self.t_idxs = np.arange(TRAJECTORY_SIZE)
|
||||
self.y_pts = np.zeros((TRAJECTORY_SIZE,))
|
||||
self.v_plan = np.zeros((TRAJECTORY_SIZE,))
|
||||
self.v_ego = 0.0
|
||||
self.x_sol = np.zeros((TRAJECTORY_SIZE, 4), dtype=np.float32)
|
||||
self.v_ego = MIN_SPEED
|
||||
self.l_lane_change_prob = 0.0
|
||||
self.r_lane_change_prob = 0.0
|
||||
self.d_path_w_lines_xyz = np.zeros((TRAJECTORY_SIZE, 3))
|
||||
@@ -91,11 +92,15 @@ class LateralPlanner:
|
||||
# Parse model predictions
|
||||
md = sm['modelV2']
|
||||
self.LP.parse_model(md)
|
||||
if len(md.position.x) == TRAJECTORY_SIZE and len(md.orientation.x) == TRAJECTORY_SIZE:
|
||||
if len(md.position.x) == TRAJECTORY_SIZE and (len(md.orientation.x) == TRAJECTORY_SIZE or
|
||||
(len(md.velocity.x) == TRAJECTORY_SIZE and len(md.lateralPlannerSolution.x) == TRAJECTORY_SIZE)):
|
||||
if len(md.orientation.x) == TRAJECTORY_SIZE:
|
||||
self.t_idxs = np.array(md.position.t)
|
||||
self.plan_yaw = np.array(md.orientation.z)
|
||||
self.plan_yaw_rate = np.array(md.orientationRate.z)
|
||||
if len(md.velocity.x) == TRAJECTORY_SIZE and len(md.lateralPlannerSolution.x) == TRAJECTORY_SIZE:
|
||||
self.x_sol = np.column_stack([md.lateralPlannerSolution.x, md.lateralPlannerSolution.y, md.lateralPlannerSolution.yaw, md.lateralPlannerSolution.yawRate])
|
||||
self.path_xyz = np.column_stack([md.position.x, md.position.y, md.position.z])
|
||||
self.t_idxs = np.array(md.position.t)
|
||||
self.plan_yaw = np.array(md.orientation.z)
|
||||
self.plan_yaw_rate = np.array(md.orientationRate.z)
|
||||
self.velocity_xyz = np.column_stack([md.velocity.x, md.velocity.y, md.velocity.z])
|
||||
car_speed = np.linalg.norm(self.velocity_xyz, axis=1) - get_speed_error(md, v_ego_car)
|
||||
self.v_plan = np.clip(car_speed, MIN_SPEED, np.inf)
|
||||
@@ -120,45 +125,46 @@ class LateralPlanner:
|
||||
self.path_xyz[:, 1] += self.LP.path_offset
|
||||
self.dynamic_lane_profile_status = True
|
||||
|
||||
self.lat_mpc.set_weights(PATH_COST, LATERAL_MOTION_COST,
|
||||
LATERAL_ACCEL_COST, LATERAL_JERK_COST,
|
||||
STEERING_RATE_COST)
|
||||
if self.dynamic_lane_profile_status:
|
||||
self.lat_mpc.set_weights(PATH_COST, LATERAL_MOTION_COST,
|
||||
LATERAL_ACCEL_COST, LATERAL_JERK_COST,
|
||||
STEERING_RATE_COST)
|
||||
|
||||
y_pts = self.path_xyz[:LAT_MPC_N+1, 1]
|
||||
heading_pts = self.plan_yaw[:LAT_MPC_N+1]
|
||||
yaw_rate_pts = self.plan_yaw_rate[:LAT_MPC_N+1]
|
||||
self.y_pts = y_pts
|
||||
y_pts = self.path_xyz[:LAT_MPC_N+1, 1]
|
||||
heading_pts = self.plan_yaw[:LAT_MPC_N+1]
|
||||
yaw_rate_pts = self.plan_yaw_rate[:LAT_MPC_N+1]
|
||||
self.y_pts = y_pts
|
||||
|
||||
assert len(y_pts) == LAT_MPC_N + 1
|
||||
assert len(heading_pts) == LAT_MPC_N + 1
|
||||
assert len(yaw_rate_pts) == LAT_MPC_N + 1
|
||||
lateral_factor = np.clip(self.factor1 - (self.factor2 * self.v_plan**2), 0.0, np.inf)
|
||||
p = np.column_stack([self.v_plan, lateral_factor])
|
||||
self.lat_mpc.run(self.x0,
|
||||
p,
|
||||
y_pts,
|
||||
heading_pts,
|
||||
yaw_rate_pts)
|
||||
# init state for next iteration
|
||||
# mpc.u_sol is the desired second derivative of psi given x0 curv state.
|
||||
# with x0[3] = measured_yaw_rate, this would be the actual desired yaw rate.
|
||||
# instead, interpolate x_sol so that x0[3] is the desired yaw rate for lat_control.
|
||||
self.x0[3] = interp(DT_MDL, self.t_idxs[:LAT_MPC_N + 1], self.lat_mpc.x_sol[:, 3])
|
||||
assert len(y_pts) == LAT_MPC_N + 1
|
||||
assert len(heading_pts) == LAT_MPC_N + 1
|
||||
assert len(yaw_rate_pts) == LAT_MPC_N + 1
|
||||
lateral_factor = np.clip(self.factor1 - (self.factor2 * self.v_plan**2), 0.0, np.inf)
|
||||
p = np.column_stack([self.v_plan, lateral_factor])
|
||||
self.lat_mpc.run(self.x0,
|
||||
p,
|
||||
y_pts,
|
||||
heading_pts,
|
||||
yaw_rate_pts)
|
||||
# init state for next iteration
|
||||
# mpc.u_sol is the desired second derivative of psi given x0 curv state.
|
||||
# with x0[3] = measured_yaw_rate, this would be the actual desired yaw rate.
|
||||
# instead, interpolate x_sol so that x0[3] is the desired yaw rate for lat_control.
|
||||
self.x0[3] = interp(DT_MDL, self.t_idxs[:LAT_MPC_N + 1], self.lat_mpc.x_sol[:, 3])
|
||||
|
||||
# Check for infeasible MPC solution
|
||||
mpc_nans = np.isnan(self.lat_mpc.x_sol[:, 3]).any()
|
||||
t = time.monotonic()
|
||||
if mpc_nans or self.lat_mpc.solution_status != 0:
|
||||
self.reset_mpc()
|
||||
self.x0[3] = measured_curvature * self.v_ego
|
||||
if t > self.last_cloudlog_t + 5.0:
|
||||
self.last_cloudlog_t = t
|
||||
cloudlog.warning("Lateral mpc - nan: True")
|
||||
# Check for infeasible MPC solution
|
||||
mpc_nans = np.isnan(self.lat_mpc.x_sol[:, 3]).any()
|
||||
t = time.monotonic()
|
||||
if mpc_nans or self.lat_mpc.solution_status != 0:
|
||||
self.reset_mpc()
|
||||
self.x0[3] = measured_curvature * self.v_ego
|
||||
if t > self.last_cloudlog_t + 5.0:
|
||||
self.last_cloudlog_t = t
|
||||
cloudlog.warning("Lateral mpc - nan: True")
|
||||
|
||||
if self.lat_mpc.cost > 1e6 or mpc_nans:
|
||||
self.solution_invalid_cnt += 1
|
||||
else:
|
||||
self.solution_invalid_cnt = 0
|
||||
if self.lat_mpc.cost > 1e6 or mpc_nans:
|
||||
self.solution_invalid_cnt += 1
|
||||
else:
|
||||
self.solution_invalid_cnt = 0
|
||||
|
||||
def get_dynamic_lane_profile(self, longitudinal_plan_sp):
|
||||
if self.dynamic_lane_profile == 1:
|
||||
@@ -191,19 +197,20 @@ class LateralPlanner:
|
||||
|
||||
lateralPlan = plan_send.lateralPlan
|
||||
lateralPlan.modelMonoTime = sm.logMonoTime['modelV2']
|
||||
lateralPlan.dPathPoints = self.y_pts.tolist()
|
||||
lateralPlan.psis = self.lat_mpc.x_sol[0:CONTROL_N, 2].tolist()
|
||||
lateralPlan.dPathPoints = self.y_pts.tolist() if self.dynamic_lane_profile_status else self.path_xyz[:,1].tolist()
|
||||
lateralPlan.psis = self.lat_mpc.x_sol[0:CONTROL_N, 2].tolist() if self.dynamic_lane_profile_status else self.x_sol[0:CONTROL_N, 2].tolist()
|
||||
|
||||
lateralPlan.curvatures = (self.lat_mpc.x_sol[0:CONTROL_N, 3]/self.v_ego).tolist()
|
||||
lateralPlan.curvatureRates = [float(x.item() / self.v_ego) for x in self.lat_mpc.u_sol[0:CONTROL_N - 1]] + [0.0]
|
||||
lateralPlan.curvatures = (self.lat_mpc.x_sol[0:CONTROL_N, 3]/self.v_ego).tolist() if self.dynamic_lane_profile_status else (self.x_sol[0:CONTROL_N, 3]/self.v_ego).tolist()
|
||||
lateralPlan.curvatureRates = [float(x.item() / self.v_ego) for x in self.lat_mpc.u_sol[0:CONTROL_N - 1]] + [0.0] if self.dynamic_lane_profile_status else [float(0) for _ in range(CONTROL_N-1)] # TODO: unused
|
||||
|
||||
lateralPlan.mpcSolutionValid = bool(plan_solution_valid)
|
||||
lateralPlan.solverExecutionTime = self.lat_mpc.solve_time
|
||||
lateralPlan.mpcSolutionValid = bool(plan_solution_valid) if self.dynamic_lane_profile_status else bool(1)
|
||||
lateralPlan.solverExecutionTime = self.lat_mpc.solve_time if self.dynamic_lane_profile_status else 0.0
|
||||
if self.debug_mode:
|
||||
lateralPlan.solverCost = self.lat_mpc.cost
|
||||
lateralPlan.solverState = log.LateralPlan.SolverState.new_message()
|
||||
lateralPlan.solverState.x = self.lat_mpc.x_sol.tolist()
|
||||
lateralPlan.solverState.u = self.lat_mpc.u_sol.flatten().tolist()
|
||||
lateralPlan.solverState.x = self.lat_mpc.x_sol.tolist() if self.dynamic_lane_profile_status else self.x_sol.tolist()
|
||||
if self.dynamic_lane_profile_status:
|
||||
lateralPlan.solverCost = self.lat_mpc.cost
|
||||
lateralPlan.solverState = log.LateralPlan.SolverState.new_message()
|
||||
lateralPlan.solverState.u = self.lat_mpc.u_sol.flatten().tolist()
|
||||
|
||||
lateralPlan.desire = self.DH.desire
|
||||
lateralPlan.useLaneLines = self.use_lanelines
|
||||
|
||||
@@ -21,8 +21,8 @@ def publish_ui_plan(sm, pm, lateral_planner, longitudinal_planner):
|
||||
ui_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'modelV2'])
|
||||
uiPlan = ui_send.uiPlan
|
||||
uiPlan.frameId = sm['modelV2'].frameId
|
||||
uiPlan.position.x = np.interp(plan_odo, model_odo, lateral_planner.lat_mpc.x_sol[:,0]).tolist()
|
||||
uiPlan.position.y = np.interp(plan_odo, model_odo, lateral_planner.lat_mpc.x_sol[:,1]).tolist()
|
||||
uiPlan.position.x = np.interp(plan_odo, model_odo, lateral_planner.x_sol[:,0]).tolist()
|
||||
uiPlan.position.y = np.interp(plan_odo, model_odo, lateral_planner.x_sol[:,1]).tolist()
|
||||
uiPlan.position.z = np.interp(plan_odo, model_odo, lateral_planner.path_xyz[:,2]).tolist()
|
||||
uiPlan.accel = longitudinal_planner.a_desired_trajectory_full.tolist()
|
||||
pm.send('uiPlan', ui_send)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import pytest
|
||||
import signal
|
||||
import time
|
||||
import unittest
|
||||
@@ -17,6 +18,7 @@ MAX_STARTUP_TIME = 3
|
||||
BLACKLIST_PROCS = ['manage_athenad', 'pandad', 'pigeond']
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestManager(unittest.TestCase):
|
||||
def setUp(self):
|
||||
os.environ['PASSIVE'] = '0'
|
||||
@@ -66,10 +68,6 @@ class TestManager(unittest.TestCase):
|
||||
|
||||
self.assertNotIn(p.name, BLACKLIST_PROCS, f"{p.name} was started")
|
||||
|
||||
# TODO: mapsd should exit cleanly
|
||||
if p.name == "mapsd":
|
||||
continue
|
||||
|
||||
self.assertTrue(exit_code is not None, f"{p.name} failed to exit")
|
||||
|
||||
# TODO: interrupted blocking read exits with 1 in cereal. use a more unique return code
|
||||
|
||||
@@ -58,11 +58,11 @@ lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"]+files, cmd)
|
||||
|
||||
# Build thneed model
|
||||
if arch == "larch64" or GetOption('pc_thneed'):
|
||||
tinygrad_opts = ["NOLOCALS=1", "IMAGE=2", "GPU=1"]
|
||||
tinygrad_opts = []
|
||||
if not GetOption('pc_thneed'):
|
||||
# use FLOAT16 on device for speed + don't cache the CL kernels for space
|
||||
tinygrad_opts += ["FLOAT16=1", "PYOPENCL_NO_CACHE=1"]
|
||||
cmd = f"cd {Dir('#').abspath}/tinygrad_repo && " + ' '.join(tinygrad_opts) + f" python3 openpilot/compile.py {fn}.onnx {fn}.thneed"
|
||||
cmd = f"cd {Dir('#').abspath}/tinygrad_repo && " + ' '.join(tinygrad_opts) + f" python3 openpilot/compile2.py {fn}.onnx {fn}.thneed"
|
||||
|
||||
tinygrad_files = sum([lenv.Glob("#"+x) for x in open(File("#release/files_common").abspath).read().split("\n") if x.startswith("tinygrad_repo/")], [])
|
||||
lenv.Command(fn + ".thneed", [fn + ".onnx"] + tinygrad_files, cmd)
|
||||
|
||||
@@ -21,6 +21,7 @@ class ModelConstants:
|
||||
NAV_FEATURE_LEN = 256
|
||||
NAV_INSTRUCTION_LEN = 150
|
||||
DRIVING_STYLE_LEN = 12
|
||||
LAT_PLANNER_STATE_LEN = 4
|
||||
|
||||
# model outputs constants
|
||||
FCW_THRESHOLDS_5MS2 = np.array([.05, .05, .15, .15, .15], dtype=np.float32)
|
||||
@@ -37,6 +38,7 @@ class ModelConstants:
|
||||
ROAD_EDGES_WIDTH = 2
|
||||
PLAN_WIDTH = 15
|
||||
DESIRE_PRED_WIDTH = 8
|
||||
LAT_PLANNER_SOLUTION_WIDTH = 4
|
||||
|
||||
NUM_LANE_LINES = 4
|
||||
NUM_ROAD_EDGES = 2
|
||||
|
||||
@@ -71,6 +71,11 @@ def fill_model_msg(msg: capnp._DynamicStructBuilder, net_output_data: Dict[str,
|
||||
orientation_rate = modelV2.orientationRate
|
||||
fill_xyzt(orientation_rate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T)
|
||||
|
||||
# lateral planning
|
||||
solution = modelV2.lateralPlannerSolution
|
||||
solution.x, solution.y, solution.yaw, solution.yawRate = [net_output_data['lat_planner_solution'][0,:,i].tolist() for i in range(4)]
|
||||
solution.xStd, solution.yStd, solution.yawStd, solution.yawRateStd = [net_output_data['lat_planner_solution_stds'][0,:,i].tolist() for i in range(4)]
|
||||
|
||||
# times at X_IDXS according to model plan
|
||||
PLAN_T_IDXS = [np.nan] * ModelConstants.IDX_N
|
||||
PLAN_T_IDXS[0] = 0.0
|
||||
@@ -87,7 +92,7 @@ def fill_model_msg(msg: capnp._DynamicStructBuilder, net_output_data: Dict[str,
|
||||
# interpolate to find `t` for the current xidx
|
||||
current_x_val = plan_x[tidx]
|
||||
next_x_val = plan_x[tidx+1]
|
||||
p = (ModelConstants.X_IDXS[xidx] - current_x_val) / (next_x_val - current_x_val)
|
||||
p = (ModelConstants.X_IDXS[xidx] - current_x_val) / (next_x_val - current_x_val) if abs(next_x_val - current_x_val) > 1e-9 else float('nan')
|
||||
PLAN_T_IDXS[xidx] = p * ModelConstants.T_IDXS[tidx+1] + (1 - p) * ModelConstants.T_IDXS[tidx]
|
||||
|
||||
# lane lines
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import pickle
|
||||
@@ -12,15 +11,19 @@ from cereal.messaging import PubMaster, SubMaster
|
||||
from cereal.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
|
||||
from openpilot.system.swaglog import cloudlog
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.numpy_fast import interp
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.realtime import config_realtime_process
|
||||
from openpilot.common.transformations.model import get_warp_matrix
|
||||
from openpilot.selfdrive import sentry
|
||||
from openpilot.selfdrive.modeld.runners import ModelRunner, Runtime
|
||||
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
|
||||
from openpilot.selfdrive.modeld.models.commonmodel_pyx import ModelFrame, CLContext
|
||||
|
||||
PROCESS_NAME = "selfdrive.modeld.modeld"
|
||||
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||
|
||||
MODEL_PATHS = {
|
||||
@@ -53,6 +56,7 @@ class ModelState:
|
||||
self.inputs = {
|
||||
'desire': np.zeros(ModelConstants.DESIRE_LEN * (ModelConstants.HISTORY_BUFFER_LEN+1), dtype=np.float32),
|
||||
'traffic_convention': np.zeros(ModelConstants.TRAFFIC_CONVENTION_LEN, dtype=np.float32),
|
||||
'lat_planner_state': np.zeros(ModelConstants.LAT_PLANNER_STATE_LEN, dtype=np.float32),
|
||||
'nav_features': np.zeros(ModelConstants.NAV_FEATURE_LEN, dtype=np.float32),
|
||||
'nav_instructions': np.zeros(ModelConstants.NAV_INSTRUCTION_LEN, dtype=np.float32),
|
||||
'features_buffer': np.zeros(ModelConstants.HISTORY_BUFFER_LEN * ModelConstants.FEATURE_LEN, dtype=np.float32),
|
||||
@@ -104,12 +108,15 @@ class ModelState:
|
||||
|
||||
self.inputs['features_buffer'][:-ModelConstants.FEATURE_LEN] = self.inputs['features_buffer'][ModelConstants.FEATURE_LEN:]
|
||||
self.inputs['features_buffer'][-ModelConstants.FEATURE_LEN:] = outputs['hidden_state'][0, :]
|
||||
self.inputs['lat_planner_state'][2] = interp(DT_MDL, ModelConstants.T_IDXS, outputs['lat_planner_solution'][0, :, 2])
|
||||
self.inputs['lat_planner_state'][3] = interp(DT_MDL, ModelConstants.T_IDXS, outputs['lat_planner_solution'][0, :, 3])
|
||||
return outputs
|
||||
|
||||
|
||||
def main():
|
||||
cloudlog.bind(daemon="selfdrive.modeld.modeld")
|
||||
setproctitle("selfdrive.modeld.modeld")
|
||||
sentry.set_tag("daemon", PROCESS_NAME)
|
||||
cloudlog.bind(daemon=PROCESS_NAME)
|
||||
setproctitle(PROCESS_NAME)
|
||||
config_realtime_process(7, 54)
|
||||
|
||||
cl_context = CLContext()
|
||||
@@ -280,4 +287,7 @@ if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
sys.exit()
|
||||
cloudlog.warning(f"child {PROCESS_NAME} got SIGINT")
|
||||
except Exception:
|
||||
sentry.capture_exception()
|
||||
raise
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:18e53fa5d0f9a185833d50fa0d777725ca715460062f3a2c050847588525d87c
|
||||
size 52272467
|
||||
oid sha256:0b7184d46dc2ba3f84eb748252634205105a3742bac29942ae4380b0332fa850
|
||||
size 52524758
|
||||
|
||||
@@ -93,6 +93,7 @@ class Parser:
|
||||
self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_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))
|
||||
self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH))
|
||||
for k in ['lead_prob', 'lane_lines_prob', 'meta']:
|
||||
self.parse_binary_crossentropy(k, outs)
|
||||
self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,))
|
||||
|
||||
@@ -13,6 +13,7 @@ from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera
|
||||
IMG = np.zeros(int(tici_f_frame_size[0]*tici_f_frame_size[1]*(3/2)), dtype=np.uint8)
|
||||
IMG_BYTES = IMG.flatten().tobytes()
|
||||
|
||||
|
||||
class TestModeld(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@@ -4,7 +4,7 @@ map_env = qt_env.Clone()
|
||||
libs = ['qt_widgets', 'qt_util', 'qmapboxgl', common, messaging, cereal, visionipc, transformations,
|
||||
'zmq', 'capnp', 'kj', 'm', 'OpenCL', 'ssl', 'crypto', 'pthread', 'json11'] + map_env["LIBS"]
|
||||
if arch == 'larch64':
|
||||
libs.append('EGL')
|
||||
libs.append(':libEGL_mesa.so.0')
|
||||
|
||||
if arch in ['larch64', 'aarch64', 'x86_64']:
|
||||
if arch == 'x86_64':
|
||||
|
||||
@@ -4,14 +4,19 @@
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
|
||||
#include "common/util.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "selfdrive/ui/qt/maps/map_helpers.h"
|
||||
#include "selfdrive/navd/map_renderer.h"
|
||||
#include "system/hardware/hw.h"
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
Hardware::config_cpu_rendering();
|
||||
|
||||
qInstallMessageHandler(swagLogMessageHandler);
|
||||
setpriority(PRIO_PROCESS, 0, -20);
|
||||
int ret = util::set_core_affinity({0, 1, 2, 3});
|
||||
assert(ret == 0);
|
||||
|
||||
QApplication app(argc, argv);
|
||||
std::signal(SIGINT, sigTermHandler);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import numpy as np
|
||||
import os
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
import threading
|
||||
@@ -16,6 +18,11 @@ CACHE_PATH = "/data/mbgl-cache-navd.db"
|
||||
LOCATION1 = (32.7174, -117.16277)
|
||||
LOCATION2 = (32.7558, -117.2037)
|
||||
|
||||
DEFAULT_ITERATIONS = 30 * LLK_DECIMATION
|
||||
|
||||
LOCATION1_REPEATED = [LOCATION1] * DEFAULT_ITERATIONS
|
||||
LOCATION2_REPEATED = [LOCATION2] * DEFAULT_ITERATIONS
|
||||
|
||||
def gen_llk(location=LOCATION1):
|
||||
msg = messaging.new_message('liveLocationKalman')
|
||||
msg.liveLocationKalman.positionGeodetic = {'value': [*location, 0], 'std': [0., 0., 0.], 'valid': True}
|
||||
@@ -27,19 +34,12 @@ def gen_llk(location=LOCATION1):
|
||||
class MapBoxInternetDisabledRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
INTERNET_ACTIVE = True
|
||||
|
||||
def setup(self):
|
||||
if self.INTERNET_ACTIVE:
|
||||
super().setup()
|
||||
|
||||
def handle(self):
|
||||
if self.INTERNET_ACTIVE:
|
||||
super().handle()
|
||||
|
||||
def finish(self):
|
||||
if self.INTERNET_ACTIVE:
|
||||
super().finish()
|
||||
|
||||
def do_GET(self):
|
||||
if not self.INTERNET_ACTIVE:
|
||||
self.send_response(500)
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
url = f'https://api.mapbox.com{self.path}'
|
||||
|
||||
headers = dict(self.headers)
|
||||
@@ -110,15 +110,14 @@ class TestMapRenderer(unittest.TestCase):
|
||||
assert self.vipc.connect(False)
|
||||
self.vipc.recv()
|
||||
|
||||
|
||||
def _run_test(self, expect_valid, location=LOCATION1):
|
||||
def _run_test(self, expect_valid, locations=LOCATION1_REPEATED):
|
||||
starting_frame_id = None
|
||||
|
||||
self.location = location
|
||||
render_times = []
|
||||
|
||||
# run test
|
||||
prev_frame_id = -1
|
||||
for i in range(30*LLK_DECIMATION):
|
||||
for i, location in enumerate(locations):
|
||||
frame_expected = (i+1) % LLK_DECIMATION == 0
|
||||
|
||||
if self.sm.logMonoTime['mapRenderState'] == 0:
|
||||
@@ -131,7 +130,7 @@ class TestMapRenderer(unittest.TestCase):
|
||||
if starting_frame_id is None:
|
||||
starting_frame_id = prev_frame_id
|
||||
|
||||
llk = gen_llk(self.location)
|
||||
llk = gen_llk(location)
|
||||
self.pm.send("liveLocationKalman", llk)
|
||||
self.pm.wait_for_readers_to_update("liveLocationKalman", 10)
|
||||
self.sm.update(1000 if frame_expected else 0)
|
||||
@@ -157,6 +156,7 @@ class TestMapRenderer(unittest.TestCase):
|
||||
assert self.sm['mapRenderState'].renderTime == 0.
|
||||
else:
|
||||
assert 0. < self.sm['mapRenderState'].renderTime < 0.1
|
||||
render_times.append(self.sm['mapRenderState'].renderTime)
|
||||
|
||||
# check vision ipc output
|
||||
assert self.vipc.recv() is not None
|
||||
@@ -164,6 +164,8 @@ class TestMapRenderer(unittest.TestCase):
|
||||
assert self.vipc.timestamp_sof == llk.logMonoTime
|
||||
assert self.vipc.frame_id == self.sm['mapRenderState'].frameId
|
||||
|
||||
return render_times
|
||||
|
||||
def test_with_internet(self):
|
||||
self._setup_test()
|
||||
self._run_test(True)
|
||||
@@ -180,13 +182,40 @@ class TestMapRenderer(unittest.TestCase):
|
||||
self.server.disable_internet()
|
||||
|
||||
# change locations to force mapsd to refetch
|
||||
self._run_test(False, LOCATION2)
|
||||
self._run_test(False, LOCATION2_REPEATED)
|
||||
|
||||
self.server.enable_internet()
|
||||
self._run_test(True, LOCATION2)
|
||||
self._run_test(True, LOCATION2_REPEATED)
|
||||
|
||||
self._run_test(True, LOCATION2_REPEATED)
|
||||
|
||||
@pytest.mark.tici
|
||||
def test_render_time_distribution(self):
|
||||
self._setup_test()
|
||||
# from location1 -> location2 and back
|
||||
locations = np.array([*np.linspace(LOCATION1, LOCATION2, 2000), *np.linspace(LOCATION2, LOCATION1, 2000)]).tolist()
|
||||
|
||||
render_times = self._run_test(True, locations)
|
||||
|
||||
_min = np.min(render_times)
|
||||
_max = np.max(render_times)
|
||||
_mean = np.mean(render_times)
|
||||
_median = np.median(render_times)
|
||||
_stddev = np.std(render_times)
|
||||
|
||||
print(f"Stats: min: {_min}, max: {_max}, mean: {_mean}, median: {_median}, stddev: {_stddev}, count: {len(render_times)}")
|
||||
|
||||
def assert_stat(stat, nominal, tol=0.3):
|
||||
tol = (nominal / (1+tol)), (nominal * (1+tol))
|
||||
self.assertTrue(tol[0] < stat < tol[1], f"{stat} not in tolerance {tol}")
|
||||
|
||||
assert_stat(_mean, 0.030)
|
||||
assert_stat(_median, 0.027)
|
||||
assert_stat(_stddev, 0.0078)
|
||||
|
||||
self.assertLess(_max, 0.065)
|
||||
self.assertGreater(_min, 0.015)
|
||||
|
||||
self.location = LOCATION1
|
||||
self._run_test(True, LOCATION2)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
if [ $1 = "base" ]; then
|
||||
if [ "$1" = "base" ]; then
|
||||
export DOCKER_IMAGE=openpilot-base
|
||||
export DOCKER_FILE=Dockerfile.openpilot_base
|
||||
elif [ $1 = "sim" ]; then
|
||||
elif [ "$1" = "sim" ]; then
|
||||
export DOCKER_IMAGE=openpilot-sim
|
||||
export DOCKER_FILE=tools/sim/Dockerfile.sim
|
||||
elif [ $1 = "prebuilt" ]; then
|
||||
elif [ "$1" = "prebuilt" ]; then
|
||||
export DOCKER_IMAGE=openpilot-prebuilt
|
||||
export DOCKER_FILE=Dockerfile.openpilot
|
||||
elif [ $1 = "cl" ]; then
|
||||
elif [ "$1" = "cl" ]; then
|
||||
export DOCKER_IMAGE=openpilot-base-cl
|
||||
export DOCKER_FILE=Dockerfile.openpilot_base_cl
|
||||
else
|
||||
echo "Invalid docker build image $1"
|
||||
echo "Invalid docker build image: '$1'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -94,6 +94,8 @@ def migrate_peripheralState(lr):
|
||||
def migrate_cameraStates(lr):
|
||||
all_msgs = []
|
||||
frame_to_encode_id = defaultdict(dict)
|
||||
# just for encodeId fallback mechanism
|
||||
min_frame_id = defaultdict(lambda: float('inf'))
|
||||
|
||||
for msg in lr:
|
||||
if msg.which() not in ["roadEncodeIdx", "wideRoadEncodeIdx", "driverEncodeIdx"]:
|
||||
@@ -111,10 +113,18 @@ def migrate_cameraStates(lr):
|
||||
continue
|
||||
|
||||
camera_state = getattr(msg, msg.which())
|
||||
min_frame_id[msg.which()] = min(min_frame_id[msg.which()], camera_state.frameId)
|
||||
|
||||
encode_id = frame_to_encode_id[msg.which()].get(camera_state.frameId)
|
||||
if encode_id is None:
|
||||
print(f"Missing encoded frame for camera feed {msg.which()} with frameId: {camera_state.frameId}")
|
||||
continue
|
||||
if len(frame_to_encode_id[msg.which()]) != 0:
|
||||
continue
|
||||
|
||||
# fallback mechanism for logs without encodeIdx (e.g. logs from before 2022 with dcamera recording disabled)
|
||||
# try to fake encode_id by subtracting lowest frameId
|
||||
encode_id = camera_state.frameId - min_frame_id[msg.which()]
|
||||
print(f"Faking encodeId to {encode_id} for camera feed {msg.which()} with frameId: {camera_state.frameId}")
|
||||
|
||||
new_msg = messaging.new_message(msg.which())
|
||||
new_camera_state = getattr(new_msg, new_msg.which())
|
||||
|
||||
@@ -23,6 +23,7 @@ MAX_FRAMES = 100 if PC else 600
|
||||
NAV_FRAMES = 50
|
||||
|
||||
NO_NAV = "NO_NAV" in os.environ
|
||||
NO_MODEL = "NO_MODEL" in os.environ
|
||||
SEND_EXTRA_INPUTS = bool(int(os.getenv("SEND_EXTRA_INPUTS", "0")))
|
||||
|
||||
|
||||
@@ -161,8 +162,10 @@ if __name__ == "__main__":
|
||||
else:
|
||||
os.environ['MAPS_HOST'] = BASE_URL.rstrip('/')
|
||||
|
||||
log_msgs = []
|
||||
# run replays
|
||||
log_msgs = model_replay(lr, frs)
|
||||
if not NO_MODEL:
|
||||
log_msgs += model_replay(lr, frs)
|
||||
if not NO_NAV:
|
||||
log_msgs += nav_model_replay(lr)
|
||||
|
||||
@@ -177,10 +180,11 @@ if __name__ == "__main__":
|
||||
cmp_log = []
|
||||
|
||||
# logs are ordered based on type: modelV2, driverStateV2, nav messages (navThumbnail, mapRenderState, navModel)
|
||||
model_start_index = next(i for i, m in enumerate(all_logs) if m.which() in ("modelV2", "cameraOdometry"))
|
||||
cmp_log += all_logs[model_start_index:model_start_index + MAX_FRAMES*2]
|
||||
dmon_start_index = next(i for i, m in enumerate(all_logs) if m.which() == "driverStateV2")
|
||||
cmp_log += all_logs[dmon_start_index:dmon_start_index + MAX_FRAMES]
|
||||
if not NO_MODEL:
|
||||
model_start_index = next(i for i, m in enumerate(all_logs) if m.which() in ("modelV2", "cameraOdometry"))
|
||||
cmp_log += all_logs[model_start_index:model_start_index + MAX_FRAMES*2]
|
||||
dmon_start_index = next(i for i, m in enumerate(all_logs) if m.which() == "driverStateV2")
|
||||
cmp_log += all_logs[dmon_start_index:dmon_start_index + MAX_FRAMES]
|
||||
if not NO_NAV:
|
||||
nav_start_index = next(i for i, m in enumerate(all_logs) if m.which() in ["navThumbnail", "mapRenderState", "navModel"])
|
||||
nav_logs = all_logs[nav_start_index:nav_start_index + NAV_FRAMES*3]
|
||||
|
||||
@@ -1 +1 @@
|
||||
0e0f55cf3bb2cf79b44adf190e6387a83deb6646
|
||||
35a05ff1b68062c9478b2adfe96f1c294bee1b6c
|
||||
|
||||
@@ -27,6 +27,7 @@ from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera
|
||||
from openpilot.selfdrive.test.process_replay.migration import migrate_all
|
||||
from openpilot.selfdrive.test.process_replay.capture import ProcessOutputCapture
|
||||
from openpilot.tools.lib.logreader import LogIterable
|
||||
from openpilot.tools.lib.framereader import BaseFrameReader
|
||||
|
||||
# Numpy gives different results based on CPU features after version 19
|
||||
NUMPY_TOLERANCE = 1e-7
|
||||
@@ -201,16 +202,15 @@ class ProcessContainer:
|
||||
|
||||
self.environ_config = environ_config
|
||||
|
||||
def _setup_vision_ipc(self, all_msgs):
|
||||
def _setup_vision_ipc(self, all_msgs: LogIterable, frs: Dict[str, Any]):
|
||||
assert len(self.cfg.vision_pubs) != 0
|
||||
|
||||
device_type = next(str(msg.initData.deviceType) for msg in all_msgs if msg.which() == "initData")
|
||||
|
||||
vipc_server = VisionIpcServer("camerad")
|
||||
streams_metas = available_streams(all_msgs)
|
||||
for meta in streams_metas:
|
||||
if meta.camera_state in self.cfg.vision_pubs:
|
||||
vipc_server.create_buffers(meta.stream, 2, False, *meta.frame_sizes[device_type])
|
||||
frame_size = (frs[meta.camera_state].w, frs[meta.camera_state].h)
|
||||
vipc_server.create_buffers(meta.stream, 2, False, *frame_size)
|
||||
vipc_server.start_listener()
|
||||
|
||||
self.vipc_server = vipc_server
|
||||
@@ -224,7 +224,7 @@ class ProcessContainer:
|
||||
|
||||
def start(
|
||||
self, params_config: Dict[str, Any], environ_config: Dict[str, Any],
|
||||
all_msgs: LogIterable,
|
||||
all_msgs: LogIterable, frs: Optional[Dict[str, BaseFrameReader]],
|
||||
fingerprint: Optional[str], capture_output: bool
|
||||
):
|
||||
with self.prefix as p:
|
||||
@@ -241,7 +241,8 @@ class ProcessContainer:
|
||||
self.sockets = [messaging.sub_sock(s, timeout=100) for s in self.cfg.subs]
|
||||
|
||||
if len(self.cfg.vision_pubs) != 0:
|
||||
self._setup_vision_ipc(all_msgs)
|
||||
assert frs is not None
|
||||
self._setup_vision_ipc(all_msgs, frs)
|
||||
assert self.vipc_server is not None
|
||||
|
||||
if capture_output:
|
||||
@@ -265,7 +266,7 @@ class ProcessContainer:
|
||||
self.prefix.clean_dirs()
|
||||
self._clean_env()
|
||||
|
||||
def run_step(self, msg: capnp._DynamicStructReader, frs: Optional[Dict[str, Any]]) -> List[capnp._DynamicStructReader]:
|
||||
def run_step(self, msg: capnp._DynamicStructReader, frs: Optional[Dict[str, BaseFrameReader]]) -> List[capnp._DynamicStructReader]:
|
||||
assert self.rc and self.pm and self.sockets and self.process.proc
|
||||
|
||||
output_msgs = []
|
||||
@@ -622,7 +623,7 @@ def replay_process_with_name(name: Union[str, Iterable[str]], lr: LogIterable, *
|
||||
|
||||
|
||||
def replay_process(
|
||||
cfg: Union[ProcessConfig, Iterable[ProcessConfig]], lr: LogIterable, frs: Optional[Dict[str, Any]] = None,
|
||||
cfg: Union[ProcessConfig, Iterable[ProcessConfig]], lr: LogIterable, frs: Optional[Dict[str, BaseFrameReader]] = None,
|
||||
fingerprint: Optional[str] = None, return_all_logs: bool = False, custom_params: Optional[Dict[str, Any]] = None,
|
||||
captured_output_store: Optional[Dict[str, Dict[str, str]]] = None, disable_progress: bool = False
|
||||
) -> List[capnp._DynamicStructReader]:
|
||||
@@ -650,7 +651,7 @@ def replay_process(
|
||||
|
||||
|
||||
def _replay_multi_process(
|
||||
cfgs: List[ProcessConfig], lr: LogIterable, frs: Optional[Dict[str, Any]], fingerprint: Optional[str],
|
||||
cfgs: List[ProcessConfig], lr: LogIterable, frs: Optional[Dict[str, BaseFrameReader]], fingerprint: Optional[str],
|
||||
custom_params: Optional[Dict[str, Any]], captured_output_store: Optional[Dict[str, Dict[str, str]]], disable_progress: bool
|
||||
) -> List[capnp._DynamicStructReader]:
|
||||
if fingerprint is not None:
|
||||
@@ -677,7 +678,7 @@ def _replay_multi_process(
|
||||
for cfg in cfgs:
|
||||
container = ProcessContainer(cfg)
|
||||
containers.append(container)
|
||||
container.start(params_config, env_config, all_msgs, fingerprint, captured_output_store is not None)
|
||||
container.start(params_config, env_config, all_msgs, frs, fingerprint, captured_output_store is not None)
|
||||
|
||||
all_pubs = {pub for container in containers for pub in container.pubs}
|
||||
all_subs = {sub for container in containers for sub in container.subs}
|
||||
|
||||
@@ -1 +1 @@
|
||||
d05b67ec66630521e8cedb90981002d57d738f6d
|
||||
dbea36698ba48429b201b138846165eb4c329b92
|
||||
@@ -3,18 +3,42 @@ import os
|
||||
import argparse
|
||||
import time
|
||||
import capnp
|
||||
import numpy as np
|
||||
|
||||
from typing import Union, Iterable, Optional, List, Any, Dict, Tuple
|
||||
|
||||
from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, FAKEDATA, ProcessConfig, replay_process, get_process_config, \
|
||||
check_openpilot_enabled, get_custom_params_from_lr
|
||||
from openpilot.selfdrive.test.process_replay.vision_meta import DRIVER_FRAME_SIZES
|
||||
from openpilot.selfdrive.test.update_ci_routes import upload_route
|
||||
from openpilot.tools.lib.route import Route
|
||||
from openpilot.tools.lib.framereader import FrameReader
|
||||
from openpilot.tools.lib.framereader import FrameReader, BaseFrameReader, FrameType
|
||||
from openpilot.tools.lib.logreader import LogReader, LogIterable
|
||||
from openpilot.tools.lib.helpers import save_log
|
||||
|
||||
|
||||
class DummyFrameReader(BaseFrameReader):
|
||||
def __init__(self, w: int, h: int, frame_count: int, pix_val: int):
|
||||
self.pix_val = pix_val
|
||||
self.w, self.h = w, h
|
||||
self.frame_count = frame_count
|
||||
self.frame_type = FrameType.raw
|
||||
|
||||
def get(self, idx, count=1, pix_fmt="yuv420p"):
|
||||
if pix_fmt == "rgb24":
|
||||
shape = (self.h, self.w, 3)
|
||||
elif pix_fmt == "nv12" or pix_fmt == "yuv420p":
|
||||
shape = (int((self.h * self.w) * 3 / 2),)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
return [np.full(shape, self.pix_val, dtype=np.uint8) for _ in range(count)]
|
||||
|
||||
@staticmethod
|
||||
def zero_dcamera():
|
||||
return DummyFrameReader(*DRIVER_FRAME_SIZES["tici"], 1200, 0)
|
||||
|
||||
|
||||
def regen_segment(
|
||||
lr: LogIterable, frs: Optional[Dict[str, Any]] = None,
|
||||
processes: Iterable[ProcessConfig] = CONFIGS, disable_tqdm: bool = False
|
||||
@@ -31,7 +55,8 @@ def regen_segment(
|
||||
|
||||
|
||||
def setup_data_readers(
|
||||
route: str, sidx: int, use_route_meta: bool, needs_driver_cam: bool = True, needs_road_cam: bool = True
|
||||
route: str, sidx: int, use_route_meta: bool,
|
||||
needs_driver_cam: bool = True, needs_road_cam: bool = True, dummy_driver_cam: bool = False
|
||||
) -> Tuple[LogReader, Dict[str, Any]]:
|
||||
if use_route_meta:
|
||||
r = Route(route)
|
||||
@@ -41,8 +66,13 @@ def setup_data_readers(
|
||||
frs['roadCameraState'] = FrameReader(r.camera_paths()[sidx])
|
||||
if needs_road_cam and len(r.ecamera_paths()) > sidx and r.ecamera_paths()[sidx] is not None:
|
||||
frs['wideRoadCameraState'] = FrameReader(r.ecamera_paths()[sidx])
|
||||
if needs_driver_cam and len(r.dcamera_paths()) > sidx and r.dcamera_paths()[sidx] is not None:
|
||||
frs['driverCameraState'] = FrameReader(r.dcamera_paths()[sidx])
|
||||
if needs_driver_cam:
|
||||
if dummy_driver_cam:
|
||||
frs['driverCameraState'] = DummyFrameReader.zero_dcamera()
|
||||
elif len(r.dcamera_paths()) > sidx and r.dcamera_paths()[sidx] is not None:
|
||||
device_type = next(str(msg.initData.deviceType) for msg in lr if msg.which() == "initData")
|
||||
assert device_type != "neo", "Driver camera not supported on neo segments. Use dummy dcamera."
|
||||
frs['driverCameraState'] = FrameReader(r.dcamera_paths()[sidx])
|
||||
else:
|
||||
lr = LogReader(f"cd:/{route.replace('|', '/')}/{sidx}/rlog.bz2")
|
||||
frs = {}
|
||||
@@ -51,14 +81,19 @@ def setup_data_readers(
|
||||
if next((True for m in lr if m.which() == "wideRoadCameraState"), False):
|
||||
frs['wideRoadCameraState'] = FrameReader(f"cd:/{route.replace('|', '/')}/{sidx}/ecamera.hevc")
|
||||
if needs_driver_cam:
|
||||
frs['driverCameraState'] = FrameReader(f"cd:/{route.replace('|', '/')}/{sidx}/dcamera.hevc")
|
||||
if dummy_driver_cam:
|
||||
frs['driverCameraState'] = DummyFrameReader.zero_dcamera()
|
||||
else:
|
||||
device_type = next(str(msg.initData.deviceType) for msg in lr if msg.which() == "initData")
|
||||
assert device_type != "neo", "Driver camera not supported on neo segments. Use dummy dcamera."
|
||||
frs['driverCameraState'] = FrameReader(f"cd:/{route.replace('|', '/')}/{sidx}/dcamera.hevc")
|
||||
|
||||
return lr, frs
|
||||
|
||||
|
||||
def regen_and_save(
|
||||
route: str, sidx: int, processes: Union[str, Iterable[str]] = "all", outdir: str = FAKEDATA,
|
||||
upload: bool = False, use_route_meta: bool = False, disable_tqdm: bool = False
|
||||
upload: bool = False, use_route_meta: bool = False, disable_tqdm: bool = False, dummy_driver_cam: bool = False
|
||||
) -> str:
|
||||
if not isinstance(processes, str) and not hasattr(processes, "__iter__"):
|
||||
raise ValueError("whitelist_proc must be a string or iterable")
|
||||
@@ -77,7 +112,8 @@ def regen_and_save(
|
||||
all_vision_pubs = {pub for cfg in replayed_processes for pub in cfg.vision_pubs}
|
||||
lr, frs = setup_data_readers(route, sidx, use_route_meta,
|
||||
needs_driver_cam="driverCameraState" in all_vision_pubs,
|
||||
needs_road_cam="roadCameraState" in all_vision_pubs or "wideRoadCameraState" in all_vision_pubs)
|
||||
needs_road_cam="roadCameraState" in all_vision_pubs or "wideRoadCameraState" in all_vision_pubs,
|
||||
dummy_driver_cam=dummy_driver_cam)
|
||||
output_logs = regen_segment(lr, frs, replayed_processes, disable_tqdm=disable_tqdm)
|
||||
|
||||
log_dir = os.path.join(outdir, time.strftime("%Y-%m-%d--%H-%M-%S--0", time.gmtime()))
|
||||
@@ -107,6 +143,7 @@ if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Generate new segments from old ones")
|
||||
parser.add_argument("--upload", action="store_true", help="Upload the new segment to the CI bucket")
|
||||
parser.add_argument("--outdir", help="log output dir", default=FAKEDATA)
|
||||
parser.add_argument("--dummy-dcamera", action='store_true', help="Use dummy blank driver camera")
|
||||
parser.add_argument("--whitelist-procs", type=comma_separated_list, default=all_procs,
|
||||
help="Comma-separated whitelist of processes to regen (e.g. controlsd,radard)")
|
||||
parser.add_argument("--blacklist-procs", type=comma_separated_list, default=[],
|
||||
@@ -117,4 +154,4 @@ if __name__ == "__main__":
|
||||
|
||||
blacklist_set = set(args.blacklist_procs)
|
||||
processes = [p for p in args.whitelist_procs if p not in blacklist_set]
|
||||
regen_and_save(args.route, args.seg, processes=processes, upload=args.upload, outdir=args.outdir)
|
||||
regen_and_save(args.route, args.seg, processes=processes, upload=args.upload, outdir=args.outdir, dummy_driver_cam=args.dummy_dcamera)
|
||||
|
||||
@@ -18,7 +18,7 @@ def regen_job(segment, upload, disable_tqdm):
|
||||
fake_dongle_id = 'regen' + ''.join(random.choice('0123456789ABCDEF') for _ in range(11))
|
||||
try:
|
||||
relr = regen_and_save(sn.route_name.canonical_name, sn.segment_num, upload=upload, use_route_meta=False,
|
||||
outdir=os.path.join(FAKEDATA, fake_dongle_id), disable_tqdm=disable_tqdm)
|
||||
outdir=os.path.join(FAKEDATA, fake_dongle_id), disable_tqdm=disable_tqdm, dummy_driver_cam=True)
|
||||
relr = '|'.join(relr.split('/')[-2:])
|
||||
return f' ("{segment[0]}", "{relr}"), '
|
||||
except Exception as e:
|
||||
|
||||
@@ -41,23 +41,23 @@ source_segments = [
|
||||
]
|
||||
|
||||
segments = [
|
||||
("BODY", "aregenECF15D9E559|2023-05-10--14-26-40--0"),
|
||||
("HYUNDAI", "aregenAB9F543F70A|2023-05-10--14-28-25--0"),
|
||||
("HYUNDAI2", "aregen39F5A028F96|2023-05-10--14-31-00--0"),
|
||||
("TOYOTA", "aregen8D6A8B36E8D|2023-05-10--14-32-38--0"),
|
||||
("TOYOTA2", "aregenB1933C49809|2023-05-10--14-34-14--0"),
|
||||
("TOYOTA3", "aregen5D9915223DC|2023-05-10--14-36-43--0"),
|
||||
("HONDA", "aregen484B732B675|2023-05-10--14-38-23--0"),
|
||||
("HONDA2", "aregenAF6ACED4713|2023-05-10--14-40-01--0"),
|
||||
("CHRYSLER", "aregen99B094E1E2E|2023-05-10--14-41-40--0"),
|
||||
("RAM", "aregen5C2487E1EEB|2023-05-10--14-44-09--0"),
|
||||
("SUBARU", "aregen98D277B792E|2023-05-10--14-46-46--0"),
|
||||
("GM", "aregen377BA28D848|2023-05-10--14-48-28--0"),
|
||||
("GM2", "aregen7CA0CC0F0C2|2023-05-10--14-51-00--0"),
|
||||
("NISSAN", "aregen7097BF01563|2023-05-10--14-52-43--0"),
|
||||
("VOLKSWAGEN", "aregen765AF3D2CB5|2023-05-10--14-54-23--0"),
|
||||
("MAZDA", "aregen3053762FF2E|2023-05-10--14-56-53--0"),
|
||||
("FORD", "aregenDDE0F89FA1E|2023-05-10--14-59-26--0"),
|
||||
("BODY", "regen997DF2697CB|2023-10-30--23-14-29--0"),
|
||||
("HYUNDAI", "regen2A9D2A8E0B4|2023-10-30--23-13-34--0"),
|
||||
("HYUNDAI2", "regen6CA24BC3035|2023-10-30--23-14-28--0"),
|
||||
("TOYOTA", "regen5C019D76307|2023-10-30--23-13-31--0"),
|
||||
("TOYOTA2", "regen5DCADA88A96|2023-10-30--23-14-57--0"),
|
||||
("TOYOTA3", "regen7204CA3A498|2023-10-30--23-15-55--0"),
|
||||
("HONDA", "regen048F8FA0B24|2023-10-30--23-15-53--0"),
|
||||
("HONDA2", "regen7D2D3F82D5B|2023-10-30--23-15-55--0"),
|
||||
("CHRYSLER", "regen7125C42780C|2023-10-30--23-16-21--0"),
|
||||
("RAM", "regen2731F3213D2|2023-10-30--23-18-11--0"),
|
||||
("SUBARU", "regen86E4C1B4DDD|2023-10-30--23-18-14--0"),
|
||||
("GM", "regenF6393D64745|2023-10-30--23-17-18--0"),
|
||||
("GM2", "regen220F830C05B|2023-10-30--23-18-39--0"),
|
||||
("NISSAN", "regen4F671F7C435|2023-10-30--23-18-40--0"),
|
||||
("VOLKSWAGEN", "regen8BDFE7307A0|2023-10-30--23-19-36--0"),
|
||||
("MAZDA", "regen2E9F1A15FD5|2023-10-30--23-20-36--0"),
|
||||
("FORD", "regen6D39E54606E|2023-10-30--23-20-54--0"),
|
||||
]
|
||||
|
||||
# dashcamOnly makes don't need to be tested until a full port is done
|
||||
|
||||
@@ -4,13 +4,12 @@ import unittest
|
||||
|
||||
from parameterized import parameterized
|
||||
|
||||
from openpilot.selfdrive.test.process_replay.regen import regen_segment
|
||||
from openpilot.selfdrive.test.process_replay.process_replay import check_openpilot_enabled, CONFIGS
|
||||
from openpilot.selfdrive.test.process_replay.regen import regen_segment, DummyFrameReader
|
||||
from openpilot.selfdrive.test.process_replay.process_replay import check_openpilot_enabled
|
||||
from openpilot.selfdrive.test.openpilotci import get_url
|
||||
from openpilot.tools.lib.logreader import LogReader
|
||||
from openpilot.tools.lib.framereader import FrameReader
|
||||
|
||||
EXCLUDED_PROCESSES = {"dmonitoringd", "dmonitoringmodeld"}
|
||||
TESTED_SEGMENTS = [
|
||||
("PRIUS_C2", "0982d79ebb0de295|2021-01-04--17-13-21--13"), # TOYOTA PRIUS 2017: NEO, pandaStateDEPRECATED, no peripheralState, sensorEventsDEPRECATED
|
||||
# Enable these once regen on CI becomes faster or use them for different tests running controlsd in isolation
|
||||
@@ -21,9 +20,9 @@ TESTED_SEGMENTS = [
|
||||
|
||||
def ci_setup_data_readers(route, sidx):
|
||||
lr = LogReader(get_url(route, sidx, "rlog"))
|
||||
# dm disabled
|
||||
frs = {
|
||||
'roadCameraState': FrameReader(get_url(route, sidx, "fcamera")),
|
||||
'driverCameraState': DummyFrameReader.zero_dcamera()
|
||||
}
|
||||
if next((True for m in lr if m.which() == "wideRoadCameraState"), False):
|
||||
frs["wideRoadCameraState"] = FrameReader(get_url(route, sidx, "ecamera"))
|
||||
@@ -34,11 +33,9 @@ def ci_setup_data_readers(route, sidx):
|
||||
class TestRegen(unittest.TestCase):
|
||||
@parameterized.expand(TESTED_SEGMENTS)
|
||||
def test_engaged(self, case_name, segment):
|
||||
tested_procs = [p for p in CONFIGS if p.proc_name not in EXCLUDED_PROCESSES]
|
||||
|
||||
route, sidx = segment.rsplit("--", 1)
|
||||
lr, frs = ci_setup_data_readers(route, sidx)
|
||||
output_logs = regen_segment(lr, frs, processes=tested_procs, disable_tqdm=True)
|
||||
output_logs = regen_segment(lr, frs, disable_tqdm=True)
|
||||
|
||||
engaged = check_openpilot_enabled(output_logs)
|
||||
self.assertTrue(engaged, f"openpilot not engaged in {case_name}")
|
||||
|
||||
@@ -43,11 +43,11 @@ while true; do
|
||||
sudo systemctl start ssh
|
||||
fi
|
||||
|
||||
if ! pgrep -f 'ciui.py' > /dev/null 2>&1; then
|
||||
echo 'starting UI'
|
||||
cp $SOURCE_DIR/selfdrive/test/ciui.py /data/
|
||||
/data/ciui.py &
|
||||
fi
|
||||
#if ! pgrep -f 'ciui.py' > /dev/null 2>&1; then
|
||||
# echo 'starting UI'
|
||||
# cp $SOURCE_DIR/selfdrive/test/ciui.py /data/
|
||||
# /data/ciui.py &
|
||||
#fi
|
||||
|
||||
sleep 5s
|
||||
done
|
||||
|
||||
@@ -3,6 +3,9 @@ import bz2
|
||||
import math
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import psutil
|
||||
import pytest
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
@@ -31,7 +34,7 @@ PROCS = {
|
||||
"./encoderd": 17.0,
|
||||
"./camerad": 14.5,
|
||||
"./locationd": 11.0,
|
||||
"./mapsd": 2.0,
|
||||
"./mapsd": 1.5,
|
||||
"selfdrive.controls.plannerd": 16.5,
|
||||
"./_ui": 18.0,
|
||||
"selfdrive.locationd.paramsd": 9.0,
|
||||
@@ -96,6 +99,7 @@ def cputime_total(ct):
|
||||
return ct.cpuUser + ct.cpuSystem + ct.cpuChildrenUser + ct.cpuChildrenSystem
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestOnroad(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
@@ -151,6 +155,8 @@ class TestOnroad(unittest.TestCase):
|
||||
cls.segments = cls.segments[:-1]
|
||||
|
||||
finally:
|
||||
cls.gpu_procs = {psutil.Process(int(f.name)).name() for f in pathlib.Path('/sys/devices/virtual/kgsl/kgsl/proc/').iterdir() if f.is_dir()}
|
||||
|
||||
if proc is not None:
|
||||
proc.terminate()
|
||||
if proc.wait(60) is None:
|
||||
@@ -296,6 +302,9 @@ class TestOnroad(unittest.TestCase):
|
||||
# expected to go up while the MSGQ buffers fill up
|
||||
self.assertLessEqual(max(mems) - min(mems), 3.0)
|
||||
|
||||
def test_gpu_usage(self):
|
||||
self.assertEqual(self.gpu_procs, {"weston", "_ui", "camerad", "selfdrive.modeld.modeld"})
|
||||
|
||||
def test_camera_processing_time(self):
|
||||
result = "\n"
|
||||
result += "------------------------------------------------\n"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import pytest
|
||||
import time
|
||||
import subprocess
|
||||
|
||||
@@ -9,6 +10,7 @@ from openpilot.common.timeout import Timeout
|
||||
from openpilot.selfdrive.test.helpers import set_params_enabled
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
def test_time_to_onroad():
|
||||
# launch
|
||||
set_params_enabled()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import datetime
|
||||
import os
|
||||
import pytest
|
||||
import time
|
||||
import tempfile
|
||||
import unittest
|
||||
@@ -13,6 +14,7 @@ from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestUpdated(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@@ -63,9 +63,9 @@ BodyWindow::BodyWindow(QWidget *parent) : fuel_filter(1.0, 5., 1. / UI_FREQ), QW
|
||||
face = new QLabel();
|
||||
face->setAlignment(Qt::AlignCenter);
|
||||
layout->addWidget(face);
|
||||
awake = new QMovie("../assets/body/awake.gif");
|
||||
awake = new QMovie("../assets/body/awake.gif", {}, this);
|
||||
awake->setCacheMode(QMovie::CacheAll);
|
||||
sleep = new QMovie("../assets/body/sleep.gif");
|
||||
sleep = new QMovie("../assets/body/sleep.gif", {}, this);
|
||||
sleep->setCacheMode(QMovie::CacheAll);
|
||||
|
||||
// record button
|
||||
|
||||
@@ -21,6 +21,7 @@ QMapboxGLSettings get_mapbox_settings() {
|
||||
|
||||
if (!Hardware::PC()) {
|
||||
settings.setCacheDatabasePath(MAPS_CACHE_PATH);
|
||||
settings.setCacheDatabaseMaximumSize(100 * 1024 * 1024);
|
||||
}
|
||||
settings.setApiBaseUrl(MAPS_HOST);
|
||||
settings.setAccessToken(get_mapbox_token());
|
||||
|
||||
@@ -105,6 +105,7 @@ void Networking::showEvent(QShowEvent *event) {
|
||||
}
|
||||
|
||||
void Networking::hideEvent(QHideEvent *event) {
|
||||
main_layout->setCurrentWidget(wifiScreen);
|
||||
wifi->stop();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ WiFiPromptWidget::WiFiPromptWidget(QWidget *parent) : QFrame(parent) {
|
||||
title_layout->setSpacing(32);
|
||||
{
|
||||
QLabel *icon = new QLabel;
|
||||
QPixmap *pixmap = new QPixmap("../assets/offroad/icon_wifi_strength_full.svg");
|
||||
icon->setPixmap(pixmap->scaledToWidth(80, Qt::SmoothTransformation));
|
||||
QPixmap pixmap("../assets/offroad/icon_wifi_strength_full.svg");
|
||||
icon->setPixmap(pixmap.scaledToWidth(80, Qt::SmoothTransformation));
|
||||
title_layout->addWidget(icon);
|
||||
|
||||
QLabel *title = new QLabel(tr("Setup Wi-Fi"));
|
||||
@@ -68,8 +68,8 @@ WiFiPromptWidget::WiFiPromptWidget(QWidget *parent) : QFrame(parent) {
|
||||
title_layout->addStretch();
|
||||
|
||||
QLabel *icon = new QLabel;
|
||||
QPixmap *pixmap = new QPixmap("../assets/offroad/icon_wifi_uploading.svg");
|
||||
icon->setPixmap(pixmap->scaledToWidth(120, Qt::SmoothTransformation));
|
||||
QPixmap pixmap("../assets/offroad/icon_wifi_uploading.svg");
|
||||
icon->setPixmap(pixmap.scaledToWidth(120, Qt::SmoothTransformation));
|
||||
title_layout->addWidget(icon);
|
||||
}
|
||||
uploading_layout->addLayout(title_layout);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import pytest
|
||||
import time
|
||||
import unittest
|
||||
import numpy as np
|
||||
@@ -8,7 +9,6 @@ import cereal.messaging as messaging
|
||||
from cereal import log
|
||||
from cereal.services import SERVICE_LIST
|
||||
from openpilot.selfdrive.manager.process_config import managed_processes
|
||||
from openpilot.system.hardware import TICI
|
||||
|
||||
TEST_TIMESPAN = 30
|
||||
LAG_FRAME_TOLERANCE = {log.FrameData.ImageSensor.ar0231: 0.5, # ARs use synced pulses for frame starts
|
||||
@@ -19,12 +19,10 @@ FRAME_DELTA_TOLERANCE = {log.FrameData.ImageSensor.ar0231: 1.0,
|
||||
CAMERAS = ('roadCameraState', 'driverCameraState', 'wideRoadCameraState')
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestCamerad(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not TICI:
|
||||
raise unittest.SkipTest
|
||||
|
||||
# run camerad and record logs
|
||||
managed_processes['camerad'].start()
|
||||
time.sleep(3)
|
||||
|
||||
@@ -34,6 +34,8 @@ public:
|
||||
static bool get_ssh_enabled() { return false; }
|
||||
static void set_ssh_enabled(bool enabled) {}
|
||||
|
||||
static void config_cpu_rendering();
|
||||
|
||||
static bool PC() { return false; }
|
||||
static bool TICI() { return false; }
|
||||
static bool AGNOS() { return false; }
|
||||
|
||||
@@ -20,4 +20,10 @@ public:
|
||||
snprintf(volume_str, sizeof(volume_str), "%.3f", volume);
|
||||
std::system(("pactl set-sink-volume @DEFAULT_SINK@ " + std::string(volume_str)).c_str());
|
||||
}
|
||||
|
||||
static void config_cpu_rendering() {
|
||||
setenv("QT_QPA_PLATFORM", "offscreen", 1);
|
||||
setenv("__GLX_VENDOR_LIBRARY_NAME", "mesa", 1);
|
||||
setenv("LP_NUM_THREADS", "0", 1); // disable threading so we stay on our assigned CPU
|
||||
}
|
||||
};
|
||||
|
||||
@@ -103,4 +103,9 @@ public:
|
||||
|
||||
static bool get_ssh_enabled() { return Params().getBool("SshEnabled"); }
|
||||
static void set_ssh_enabled(bool enabled) { Params().putBool("SshEnabled", enabled); }
|
||||
|
||||
static void config_cpu_rendering() {
|
||||
setenv("QT_QPA_PLATFORM", "eglfs", 1); // offscreen doesn't work with EGL/GLES
|
||||
setenv("LP_NUM_THREADS", "0", 1); // disable threading so we stay on our assigned CPU
|
||||
}
|
||||
};
|
||||
|
||||
@@ -505,7 +505,7 @@ class Tici(HardwareBase):
|
||||
sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_rail_on")
|
||||
sudo_write("1000", "/sys/class/kgsl/kgsl-3d0/idle_timer")
|
||||
sudo_write("performance", "/sys/class/kgsl/kgsl-3d0/devfreq/governor")
|
||||
sudo_write("596", "/sys/class/kgsl/kgsl-3d0/max_clock_mhz")
|
||||
sudo_write("710", "/sys/class/kgsl/kgsl-3d0/max_clock_mhz")
|
||||
|
||||
# setup governors
|
||||
sudo_write("performance", "/sys/class/devfreq/soc:qcom,cpubw/governor")
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
import pytest
|
||||
import time
|
||||
import unittest
|
||||
import numpy as np
|
||||
|
||||
from openpilot.system.hardware import TICI
|
||||
from openpilot.system.hardware.tici.hardware import Tici
|
||||
|
||||
HARDWARE = Tici()
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestHardware(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not TICI:
|
||||
raise unittest.SkipTest
|
||||
|
||||
HARDWARE.initialize_hardware()
|
||||
HARDWARE.set_power_save(False)
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
import pytest
|
||||
import unittest
|
||||
import time
|
||||
import math
|
||||
import threading
|
||||
import numpy as np
|
||||
from dataclasses import dataclass
|
||||
from tabulate import tabulate
|
||||
from typing import List
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from cereal.services import SERVICE_LIST
|
||||
from openpilot.system.hardware import HARDWARE, TICI
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.hardware.tici.power_monitor import get_power
|
||||
from openpilot.selfdrive.manager.process_config import managed_processes
|
||||
from openpilot.selfdrive.manager.manager import manager_cleanup
|
||||
@@ -28,7 +29,7 @@ class Proc:
|
||||
|
||||
PROCS = [
|
||||
Proc('camerad', 2.1, msgs=['roadCameraState', 'wideRoadCameraState', 'driverCameraState']),
|
||||
Proc('modeld', 1.0, atol=0.2, msgs=['modelV2']),
|
||||
Proc('modeld', 1.12, atol=0.2, msgs=['modelV2']),
|
||||
Proc('dmonitoringmodeld', 0.4, msgs=['driverStateV2']),
|
||||
Proc('encoderd', 0.23, msgs=[]),
|
||||
Proc('mapsd', 0.05, msgs=['mapRenderState']),
|
||||
@@ -44,13 +45,9 @@ def send_llk_msg(done):
|
||||
time.sleep(1/20.)
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestPowerDraw(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not TICI:
|
||||
raise unittest.SkipTest
|
||||
|
||||
def setUp(self):
|
||||
HARDWARE.initialize_hardware()
|
||||
HARDWARE.set_power_save(False)
|
||||
@@ -94,8 +91,8 @@ class TestPowerDraw(unittest.TestCase):
|
||||
msgs_expected = int(sum(SAMPLE_TIME * SERVICE_LIST[msg].frequency for msg in proc.msgs))
|
||||
tab.append([proc.name, round(expected, 2), round(cur, 2), msgs_expected, msgs_received])
|
||||
with self.subTest(proc=proc.name):
|
||||
self.assertTrue(math.isclose(cur, expected, rel_tol=proc.rtol, abs_tol=proc.atol))
|
||||
self.assertTrue(math.isclose(msgs_expected, msgs_received, rel_tol=.02, abs_tol=2))
|
||||
np.testing.assert_allclose(cur, expected, rtol=proc.rtol, atol=proc.atol)
|
||||
np.testing.assert_allclose(msgs_expected, msgs_received, rtol=.02, atol=2)
|
||||
print(tabulate(tab))
|
||||
print(f"Baseline {baseline:.2f}W\n")
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import math
|
||||
import os
|
||||
import pytest
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -31,14 +32,9 @@ CAMERAS = [
|
||||
FILE_SIZE_TOLERANCE = 0.5
|
||||
|
||||
|
||||
@pytest.mark.tici # TODO: all of loggerd should work on PC
|
||||
class TestEncoder(unittest.TestCase):
|
||||
|
||||
# TODO: all of loggerd should work on PC
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not TICI:
|
||||
raise unittest.SkipTest
|
||||
|
||||
def setUp(self):
|
||||
self._clear_logs()
|
||||
os.environ["LOGGERD_TEST"] = "1"
|
||||
|
||||
@@ -190,7 +190,13 @@ def setup_quectel(diag: ModemDiag) -> bool:
|
||||
|
||||
if gps_enabled():
|
||||
at_cmd("AT+QGPSEND")
|
||||
#at_cmd("AT+QGPSDEL=0")
|
||||
|
||||
if "GPS_COLD_START" in os.environ:
|
||||
# deletes all assistance
|
||||
at_cmd("AT+QGPSDEL=0")
|
||||
else:
|
||||
# allow module to perform hot start
|
||||
at_cmd("AT+QGPSDEL=1")
|
||||
|
||||
# disable DPO power savings for more accuracy
|
||||
at_cmd("AT+QGPSCFG=\"dpoenable\",0")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import pytest
|
||||
import json
|
||||
import time
|
||||
import datetime
|
||||
@@ -7,19 +8,16 @@ import unittest
|
||||
import subprocess
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from openpilot.system.hardware import TICI
|
||||
from openpilot.system.sensord.rawgps.rawgpsd import at_cmd, wait_for_modem
|
||||
from openpilot.selfdrive.manager.process_config import managed_processes
|
||||
|
||||
GOOD_SIGNAL = bool(int(os.getenv("GOOD_SIGNAL", '0')))
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestRawgpsd(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not TICI:
|
||||
raise unittest.SkipTest
|
||||
|
||||
os.system("sudo systemctl start systemd-resolved")
|
||||
os.system("sudo systemctl restart ModemManager lte")
|
||||
wait_for_modem()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import pytest
|
||||
import time
|
||||
import unittest
|
||||
|
||||
@@ -7,16 +8,12 @@ from cereal.services import SERVICE_LIST
|
||||
from openpilot.common.gpio import gpio_read
|
||||
from openpilot.selfdrive.test.helpers import with_processes
|
||||
from openpilot.selfdrive.manager.process_config import managed_processes
|
||||
from openpilot.system.hardware import TICI
|
||||
from openpilot.system.hardware.tici.pins import GPIO
|
||||
|
||||
|
||||
# TODO: test TTFF when we have good A-GNSS
|
||||
@pytest.mark.tici
|
||||
class TestPigeond(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not TICI:
|
||||
raise unittest.SkipTest
|
||||
|
||||
def tearDown(self):
|
||||
managed_processes['pigeond'].stop()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import pytest
|
||||
import time
|
||||
import unittest
|
||||
import numpy as np
|
||||
@@ -10,7 +11,6 @@ from cereal import log
|
||||
from cereal.services import SERVICE_LIST
|
||||
from openpilot.common.gpio import get_irqs_for_action
|
||||
from openpilot.common.timeout import Timeout
|
||||
from openpilot.system.hardware import TICI
|
||||
from openpilot.selfdrive.manager.process_config import managed_processes
|
||||
|
||||
BMX = {
|
||||
@@ -98,12 +98,10 @@ def read_sensor_events(duration_sec):
|
||||
|
||||
return {k: v for k, v in events.items() if len(v) > 0}
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestSensord(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not TICI:
|
||||
raise unittest.SkipTest
|
||||
|
||||
# enable LSM self test
|
||||
os.environ["LSM_SELF_TEST"] = "1"
|
||||
|
||||
|
||||
+1
-1
Submodule tinygrad_repo updated: 5a4a62ecae...ae5d1407ee
@@ -26,6 +26,7 @@ git clone --recurse-submodules https://github.com/commaai/openpilot.git
|
||||
|
||||
``` bash
|
||||
cd openpilot
|
||||
git lfs pull
|
||||
tools/ubuntu_setup.sh
|
||||
```
|
||||
|
||||
|
||||
@@ -89,18 +89,18 @@ export const constraints = {
|
||||
export function createDummyVideoTrack() {
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
|
||||
const frameWidth = 5; // Set the width of the frame
|
||||
const frameHeight = 5; // Set the height of the frame
|
||||
canvas.width = frameWidth;
|
||||
canvas.height = frameHeight;
|
||||
|
||||
|
||||
context.fillStyle = 'black';
|
||||
context.fillRect(0, 0, frameWidth, frameHeight);
|
||||
|
||||
|
||||
const stream = canvas.captureStream();
|
||||
const videoTrack = stream.getVideoTracks()[0];
|
||||
|
||||
|
||||
return videoTrack;
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ export function start(pc, dc) {
|
||||
// add audio track
|
||||
navigator.mediaDevices.enumerateDevices()
|
||||
.then(function(devices) {
|
||||
const hasAudioInput = devices.find((device) => { device.kind === "audioinput" });
|
||||
const hasAudioInput = devices.find((device) => device.kind === "audioinput");
|
||||
var modifiedConstraints = {};
|
||||
modifiedConstraints.video = constraints.video;
|
||||
modifiedConstraints.audio = hasAudioInput ? constraints.audio : false;
|
||||
@@ -143,7 +143,7 @@ export function start(pc, dc) {
|
||||
// const dummyMediaStream = new MediaStream();
|
||||
// dummyMediaStream.addTrack(dummyVideoTrack);
|
||||
// pc.addTrack(dummyVideoTrack, dummyMediaStream);
|
||||
|
||||
|
||||
// setInterval(() => {pc.getStats(null).then((stats) => {stats.forEach((report) => console.log(report))})}, 10000)
|
||||
// var video = document.querySelector('video');
|
||||
// var print = function (e, f){console.log(e, f); video.requestVideoFrameCallback(print);};
|
||||
@@ -167,7 +167,7 @@ export function start(pc, dc) {
|
||||
var message = JSON.stringify({type: 'battery_level'});
|
||||
dc.send(message);
|
||||
}
|
||||
|
||||
|
||||
dc.onopen = function() {
|
||||
dcInterval = setInterval(controlCommand, 50);
|
||||
batteryInterval = setInterval(batteryLevel, 10000);
|
||||
|
||||
@@ -2,6 +2,5 @@ moc_*
|
||||
*.moc
|
||||
|
||||
cabana
|
||||
settings
|
||||
dbc/car_fingerprint_to_dbc.json
|
||||
tests/test_cabana
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import os
|
||||
Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', 'replay_lib',
|
||||
'cereal', 'transformations', 'widgets')
|
||||
Import('qt_env', 'arch', 'common', 'messaging', 'visionipc', 'replay_lib', 'cereal', 'widgets')
|
||||
|
||||
base_frameworks = qt_env['FRAMEWORKS']
|
||||
base_libs = [common, messaging, cereal, visionipc, transformations, 'zmq',
|
||||
'capnp', 'kj', 'm', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"]
|
||||
base_libs = [common, messaging, cereal, visionipc, 'qt_util', 'zmq', 'capnp', 'kj', 'm', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"]
|
||||
|
||||
if arch == "Darwin":
|
||||
base_frameworks.append('OpenCL')
|
||||
@@ -18,8 +15,7 @@ else:
|
||||
qt_libs = ['qt_util'] + base_libs
|
||||
|
||||
cabana_env = qt_env.Clone()
|
||||
cabana_env["LIBPATH"] += ['../../opendbc/can']
|
||||
cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, 'panda', 'libdbc_static', 'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv', 'usb-1.0'] + qt_libs
|
||||
cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, 'panda', 'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv', 'usb-1.0'] + qt_libs
|
||||
opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../opendbc").abspath)
|
||||
cabana_env['CXXFLAGS'] += [opendbc_path]
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QFontDatabase>
|
||||
#include <QHeaderView>
|
||||
#include <QMouseEvent>
|
||||
@@ -273,7 +274,7 @@ void BinaryViewModel::refresh() {
|
||||
row_count = can->lastMessage(msg_id).dat.size();
|
||||
items.resize(row_count * column_count);
|
||||
}
|
||||
int valid_rows = std::min(can->lastMessage(msg_id).dat.size(), row_count);
|
||||
int valid_rows = std::min<int>(can->lastMessage(msg_id).dat.size(), row_count);
|
||||
for (int i = 0; i < valid_rows * column_count; ++i) {
|
||||
items[i].valid = true;
|
||||
}
|
||||
@@ -311,7 +312,7 @@ void BinaryViewModel::updateState() {
|
||||
int val = ((binary[i] >> (7 - j)) & 1) != 0 ? 1 : 0;
|
||||
// Bit update frequency based highlighting
|
||||
double offset = !item.sigs.empty() ? 50 : 0;
|
||||
auto n = last_msg.bit_change_counts[i][7 - j];
|
||||
auto n = last_msg.last_changes[i].bit_change_counts[j];
|
||||
double min_f = n == 0 ? offset : offset + 25;
|
||||
double alpha = std::clamp(offset + log2(1.0 + factor * (double)n / (double)last_msg.count) * scaler, min_f, max_f);
|
||||
auto color = item.bg_color;
|
||||
@@ -334,13 +335,8 @@ QVariant BinaryViewModel::headerData(int section, Qt::Orientation orientation, i
|
||||
}
|
||||
|
||||
QVariant BinaryViewModel::data(const QModelIndex &index, int role) const {
|
||||
if (role == Qt::ToolTipRole) {
|
||||
auto item = (const BinaryViewModel::Item *)index.internalPointer();
|
||||
if (item && !item->sigs.empty()) {
|
||||
return signalToolTip(item->sigs.back());
|
||||
}
|
||||
}
|
||||
return {};
|
||||
auto item = (const BinaryViewModel::Item *)index.internalPointer();
|
||||
return role == Qt::ToolTipRole && item && !item->sigs.empty() ? signalToolTip(item->sigs.back()) : QVariant();
|
||||
}
|
||||
|
||||
// BinaryItemDelegate
|
||||
@@ -388,7 +384,7 @@ void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op
|
||||
drawSignalCell(painter, option, index, s);
|
||||
}
|
||||
}
|
||||
} else if (item->valid) {
|
||||
} else if (item->valid && item->bg_color.alpha() > 0) {
|
||||
painter->fillRect(option.rect, item->bg_color);
|
||||
}
|
||||
auto color_role = item->sigs.contains(bin_view->hovered_sig) ? QPalette::BrightText : QPalette::Text;
|
||||
|
||||
@@ -18,8 +18,6 @@ int main(int argc, char *argv[]) {
|
||||
app.setWindowIcon(QIcon(":cabana-icon.png"));
|
||||
|
||||
UnixSignalHandler signalHandler;
|
||||
|
||||
settings.load();
|
||||
utils::setTheme(settings.theme);
|
||||
|
||||
QCommandLineParser cmd_parser;
|
||||
@@ -28,6 +26,7 @@ int main(int argc, char *argv[]) {
|
||||
cmd_parser.addOption({"demo", "use a demo route instead of providing your own"});
|
||||
cmd_parser.addOption({"qcam", "load qcamera"});
|
||||
cmd_parser.addOption({"ecam", "load wide road camera"});
|
||||
cmd_parser.addOption({"dcam", "load driver camera"});
|
||||
cmd_parser.addOption({"stream", "read can messages from live streaming"});
|
||||
cmd_parser.addOption({"panda", "read can messages from panda"});
|
||||
cmd_parser.addOption({"panda-serial", "read can messages from panda with given serial", "panda-serial"});
|
||||
@@ -62,13 +61,10 @@ int main(int argc, char *argv[]) {
|
||||
stream = new SocketCanStream(&app, config);
|
||||
} else {
|
||||
uint32_t replay_flags = REPLAY_FLAG_NONE;
|
||||
if (cmd_parser.isSet("ecam")) {
|
||||
replay_flags |= REPLAY_FLAG_ECAM;
|
||||
} else if (cmd_parser.isSet("qcam")) {
|
||||
replay_flags |= REPLAY_FLAG_QCAMERA;
|
||||
} else if (cmd_parser.isSet("no-vipc")) {
|
||||
replay_flags |= REPLAY_FLAG_NO_VIPC;
|
||||
}
|
||||
if (cmd_parser.isSet("ecam")) replay_flags |= REPLAY_FLAG_ECAM;
|
||||
if (cmd_parser.isSet("qcam")) replay_flags |= REPLAY_FLAG_QCAMERA;
|
||||
if (cmd_parser.isSet("dcam")) replay_flags |= REPLAY_FLAG_DCAM;
|
||||
if (cmd_parser.isSet("no-vipc")) replay_flags |= REPLAY_FLAG_NO_VIPC;
|
||||
|
||||
const QStringList args = cmd_parser.positionalArguments();
|
||||
QString route;
|
||||
|
||||
@@ -25,20 +25,18 @@ const int AXIS_X_TOP_MARGIN = 4;
|
||||
static inline bool xLessThan(const QPointF &p, float x) { return p.x() < x; }
|
||||
|
||||
ChartView::ChartView(const std::pair<double, double> &x_range, ChartsWidget *parent)
|
||||
: charts_widget(parent), tip_label(this), QChartView(nullptr, parent) {
|
||||
: charts_widget(parent), tip_label(this), QChartView(parent) {
|
||||
series_type = (SeriesType)settings.chart_series_type;
|
||||
QChart *chart = new QChart();
|
||||
chart->setBackgroundVisible(false);
|
||||
chart()->setBackgroundVisible(false);
|
||||
axis_x = new QValueAxis(this);
|
||||
axis_y = new QValueAxis(this);
|
||||
chart->addAxis(axis_x, Qt::AlignBottom);
|
||||
chart->addAxis(axis_y, Qt::AlignLeft);
|
||||
chart->legend()->layout()->setContentsMargins(0, 0, 0, 0);
|
||||
chart->legend()->setShowToolTips(true);
|
||||
chart->setMargins({0, 0, 0, 0});
|
||||
chart()->addAxis(axis_x, Qt::AlignBottom);
|
||||
chart()->addAxis(axis_y, Qt::AlignLeft);
|
||||
chart()->legend()->layout()->setContentsMargins(0, 0, 0, 0);
|
||||
chart()->legend()->setShowToolTips(true);
|
||||
chart()->setMargins({0, 0, 0, 0});
|
||||
|
||||
axis_x->setRange(x_range.first, x_range.second);
|
||||
setChart(chart);
|
||||
|
||||
createToolButtons();
|
||||
setRubberBand(QChartView::HorizontalRubberBand);
|
||||
|
||||
@@ -100,7 +100,7 @@ ChartsWidget::ChartsWidget(QWidget *parent) : align_timer(this), auto_scroll_tim
|
||||
QObject::connect(&auto_scroll_timer, &QTimer::timeout, this, &ChartsWidget::doAutoScroll);
|
||||
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &ChartsWidget::removeAll);
|
||||
QObject::connect(can, &AbstractStream::eventsMerged, this, &ChartsWidget::eventsMerged);
|
||||
QObject::connect(can, &AbstractStream::updated, this, &ChartsWidget::updateState);
|
||||
QObject::connect(can, &AbstractStream::msgsReceived, this, &ChartsWidget::updateState);
|
||||
QObject::connect(range_slider, &QSlider::valueChanged, this, &ChartsWidget::setMaxChartRange);
|
||||
QObject::connect(new_plot_btn, &QToolButton::clicked, this, &ChartsWidget::newChart);
|
||||
QObject::connect(remove_all_btn, &QToolButton::clicked, this, &ChartsWidget::removeAll);
|
||||
@@ -324,7 +324,7 @@ void ChartsWidget::updateLayout(bool force) {
|
||||
charts_layout->addWidget(current_charts[i], i / n, i % n);
|
||||
if (current_charts[i]->sigs.empty()) {
|
||||
// the chart will be resized after add signal. delay setVisible to reduce flicker.
|
||||
QTimer::singleShot(0, [c = current_charts[i]]() { c->setVisible(true); });
|
||||
QTimer::singleShot(0, current_charts[i], [c = current_charts[i]]() { c->setVisible(true); });
|
||||
} else {
|
||||
current_charts[i]->setVisible(true);
|
||||
}
|
||||
|
||||
@@ -44,9 +44,9 @@ SignalSelector::SignalSelector(QString title, QWidget *parent) : QDialog(parent)
|
||||
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
main_layout->addWidget(buttonBox, 3, 2);
|
||||
|
||||
for (auto it = can->last_msgs.cbegin(); it != can->last_msgs.cend(); ++it) {
|
||||
if (auto m = dbc()->msg(it.key())) {
|
||||
msgs_combo->addItem(QString("%1 (%2)").arg(m->name).arg(it.key().toString()), QVariant::fromValue(it.key()));
|
||||
for (const auto &[id, _] : can->lastMessages()) {
|
||||
if (auto m = dbc()->msg(id)) {
|
||||
msgs_combo->addItem(QString("%1 (%2)").arg(m->name).arg(id.toString()), QVariant::fromValue(id));
|
||||
}
|
||||
}
|
||||
msgs_combo->model()->sort(0);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "tools/cabana/chart/sparkline.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <QPainter>
|
||||
|
||||
@@ -12,52 +13,30 @@ void Sparkline::update(const MessageId &msg_id, const cabana::Signal *sig, doubl
|
||||
auto first = std::lower_bound(msgs.cbegin(), msgs.cend(), first_ts, CompareCanEvent());
|
||||
auto last = std::upper_bound(first, msgs.cend(), ts, CompareCanEvent());
|
||||
|
||||
bool update_values = last_ts != last_msg_ts || time_range != range;
|
||||
last_ts = last_msg_ts;
|
||||
time_range = range;
|
||||
|
||||
if (first != last) {
|
||||
if (update_values) {
|
||||
values.clear();
|
||||
if (values.capacity() < std::distance(first, last)) {
|
||||
values.reserve(std::distance(first, last) * 2);
|
||||
}
|
||||
min_val = std::numeric_limits<double>::max();
|
||||
max_val = std::numeric_limits<double>::lowest();
|
||||
for (auto it = first; it != last; ++it) {
|
||||
const CanEvent *e = *it;
|
||||
double value = 0;
|
||||
if (sig->getValue(e->dat, e->size, &value)) {
|
||||
values.emplace_back((e->mono_time - (*first)->mono_time) / 1e9, value);
|
||||
if (min_val > value) min_val = value;
|
||||
if (max_val < value) max_val = value;
|
||||
}
|
||||
}
|
||||
if (min_val == max_val) {
|
||||
min_val -= 1;
|
||||
max_val += 1;
|
||||
if (first != last && !size.isEmpty()) {
|
||||
points.clear();
|
||||
double value = 0;
|
||||
for (auto it = first; it != last; ++it) {
|
||||
if (sig->getValue((*it)->dat, (*it)->size, &value)) {
|
||||
points.emplace_back(((*it)->mono_time - (*first)->mono_time) / 1e9, value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
values.clear();
|
||||
}
|
||||
|
||||
if (!values.empty()) {
|
||||
render(sig->color, size);
|
||||
const auto [min, max] = std::minmax_element(points.begin(), points.end(),
|
||||
[](auto &l, auto &r) { return l.y() < r.y(); });
|
||||
min_val = min->y() == max->y() ? min->y() - 1 : min->y();
|
||||
max_val = min->y() == max->y() ? max->y() + 1 : max->y();
|
||||
freq_ = points.size() / std::max(points.back().x() - points.front().x(), 1.0);
|
||||
render(sig->color, range, size);
|
||||
} else {
|
||||
pixmap = QPixmap();
|
||||
min_val = -1;
|
||||
max_val = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void Sparkline::render(const QColor &color, QSize size) {
|
||||
const double xscale = (size.width() - 1) / (double)time_range;
|
||||
void Sparkline::render(const QColor &color, int range, QSize size) {
|
||||
const double xscale = (size.width() - 1) / (double)range;
|
||||
const double yscale = (size.height() - 3) / (max_val - min_val);
|
||||
points.clear();
|
||||
points.reserve(values.capacity());
|
||||
for (auto &v : values) {
|
||||
points.emplace_back(v.x() * xscale, 1 + std::abs(v.y() - max_val) * yscale);
|
||||
for (auto &v : points) {
|
||||
v = QPoint(v.x() * xscale, 1 + std::abs(v.y() - max_val) * yscale);
|
||||
}
|
||||
|
||||
qreal dpr = qApp->devicePixelRatio();
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QPixmap>
|
||||
#include <QPointF>
|
||||
#include <vector>
|
||||
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/dbc/dbc.h"
|
||||
|
||||
class Sparkline {
|
||||
public:
|
||||
void update(const MessageId &msg_id, const cabana::Signal *sig, double last_msg_ts, int range, QSize size);
|
||||
const QSize size() const { return pixmap.size() / pixmap.devicePixelRatio(); }
|
||||
inline double freq() const {
|
||||
return values.empty() ? 0 : values.size() / std::max(values.back().x() - values.front().x(), 1.0);
|
||||
}
|
||||
inline double freq() const { return freq_; }
|
||||
bool isEmpty() const { return pixmap.isNull(); }
|
||||
|
||||
QPixmap pixmap;
|
||||
double min_val = 0;
|
||||
double max_val = 0;
|
||||
double last_ts = 0;
|
||||
int time_range = 0;
|
||||
|
||||
private:
|
||||
void render(const QColor &color, QSize size);
|
||||
std::vector<QPointF> values;
|
||||
void render(const QColor &color, int range, QSize size);
|
||||
|
||||
std::vector<QPointF> points;
|
||||
double freq_ = 0;
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include <QMetaType>
|
||||
#include <QString>
|
||||
|
||||
#include "opendbc/can/common_dbc.h"
|
||||
|
||||
const QString UNTITLED = "untitled";
|
||||
const QString DEFAULT_NODE_NAME = "XXX";
|
||||
@@ -120,5 +119,4 @@ public:
|
||||
double get_raw_value(const uint8_t *data, size_t data_size, const cabana::Signal &sig);
|
||||
void updateMsbLsb(cabana::Signal &s);
|
||||
inline int flipBitPos(int start_bit) { return 8 * (start_bit / 8) + 7 - start_bit % 8; }
|
||||
inline std::vector<std::string> allDBCNames() { return get_dbc_names(); }
|
||||
inline QString doubleToString(double value) { return QString::number(value, 'g', std::numeric_limits<double>::digits10); }
|
||||
|
||||
@@ -4,10 +4,8 @@
|
||||
#include <QFileInfo>
|
||||
#include <QRegularExpression>
|
||||
#include <QTextStream>
|
||||
#include <numeric>
|
||||
#include <sstream>
|
||||
|
||||
DBCFile::DBCFile(const QString &dbc_file_name, QObject *parent) : QObject(parent) {
|
||||
DBCFile::DBCFile(const QString &dbc_file_name) {
|
||||
QFile file(dbc_file_name);
|
||||
if (file.open(QIODevice::ReadOnly)) {
|
||||
name_ = QFileInfo(dbc_file_name).baseName();
|
||||
@@ -22,7 +20,7 @@ DBCFile::DBCFile(const QString &dbc_file_name, QObject *parent) : QObject(parent
|
||||
}
|
||||
}
|
||||
|
||||
DBCFile::DBCFile(const QString &name, const QString &content, QObject *parent) : QObject(parent), name_(name), filename("") {
|
||||
DBCFile::DBCFile(const QString &name, const QString &content) : name_(name), filename("") {
|
||||
// Open from clipboard
|
||||
parse(content);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <QObject>
|
||||
|
||||
#include "tools/cabana/dbc/dbc.h"
|
||||
|
||||
const QString AUTO_SAVE_EXTENSION = ".tmp";
|
||||
|
||||
class DBCFile : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
class DBCFile {
|
||||
public:
|
||||
DBCFile(const QString &dbc_file_name, QObject *parent=nullptr);
|
||||
DBCFile(const QString &name, const QString &content, QObject *parent=nullptr);
|
||||
DBCFile(const QString &dbc_file_name);
|
||||
DBCFile(const QString &name, const QString &content);
|
||||
~DBCFile() {}
|
||||
|
||||
bool save();
|
||||
|
||||
@@ -7,7 +7,7 @@ bool DBCManager::open(const SourceSet &sources, const QString &dbc_file_name, QS
|
||||
try {
|
||||
auto it = std::find_if(dbc_files.begin(), dbc_files.end(),
|
||||
[&](auto &f) { return f.second && f.second->filename == dbc_file_name; });
|
||||
auto file = (it != dbc_files.end()) ? it->second : std::make_shared<DBCFile>(dbc_file_name, this);
|
||||
auto file = (it != dbc_files.end()) ? it->second : std::make_shared<DBCFile>(dbc_file_name);
|
||||
for (auto s : sources) {
|
||||
dbc_files[s] = file;
|
||||
}
|
||||
@@ -22,7 +22,7 @@ bool DBCManager::open(const SourceSet &sources, const QString &dbc_file_name, QS
|
||||
|
||||
bool DBCManager::open(const SourceSet &sources, const QString &name, const QString &content, QString *error) {
|
||||
try {
|
||||
auto file = std::make_shared<DBCFile>(name, content, this);
|
||||
auto file = std::make_shared<DBCFile>(name, content);
|
||||
for (auto s : sources) {
|
||||
dbc_files[s] = file;
|
||||
}
|
||||
@@ -189,6 +189,13 @@ const SourceSet DBCManager::sources(const DBCFile *dbc_file) const {
|
||||
return sources;
|
||||
}
|
||||
|
||||
QString toString(const SourceSet &ss) {
|
||||
return std::accumulate(ss.cbegin(), ss.cend(), QString(), [](QString str, int source) {
|
||||
if (!str.isEmpty()) str += ", ";
|
||||
return str + (source == -1 ? QStringLiteral("all") : QString::number(source));
|
||||
});
|
||||
}
|
||||
|
||||
DBCManager *dbc() {
|
||||
static DBCManager dbc_manager(nullptr);
|
||||
return &dbc_manager;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <set>
|
||||
@@ -66,15 +67,8 @@ private:
|
||||
|
||||
DBCManager *dbc();
|
||||
|
||||
QString toString(const SourceSet &ss);
|
||||
inline QString msgName(const MessageId &id) {
|
||||
auto msg = dbc()->msg(id);
|
||||
return msg ? msg->name : UNTITLED;
|
||||
}
|
||||
|
||||
inline QString toString(const SourceSet &ss) {
|
||||
QStringList ret;
|
||||
for (auto s : ss) {
|
||||
ret << (s == -1 ? QString("all") : QString::number(s));
|
||||
}
|
||||
return ret.join(", ");
|
||||
}
|
||||
|
||||
@@ -147,8 +147,8 @@ void DetailWidget::refresh() {
|
||||
warning_widget->setVisible(!warnings.isEmpty());
|
||||
}
|
||||
|
||||
void DetailWidget::updateState(const QHash<MessageId, CanData> *msgs) {
|
||||
if ((msgs && !msgs->contains(msg_id)))
|
||||
void DetailWidget::updateState(const std::set<MessageId> *msgs) {
|
||||
if ((msgs && !msgs->count(msg_id)))
|
||||
return;
|
||||
|
||||
if (tab_widget->currentIndex() == 0)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QSplitter>
|
||||
#include <QTabWidget>
|
||||
#include <QTextEdit>
|
||||
#include <set>
|
||||
|
||||
#include "selfdrive/ui/qt/widgets/controls.h"
|
||||
#include "tools/cabana/binaryview.h"
|
||||
@@ -11,7 +12,6 @@
|
||||
#include "tools/cabana/historylog.h"
|
||||
#include "tools/cabana/signalview.h"
|
||||
|
||||
class MainWindow;
|
||||
class EditMessageDialog : public QDialog {
|
||||
public:
|
||||
EditMessageDialog(const MessageId &msg_id, const QString &title, int size, QWidget *parent);
|
||||
@@ -39,7 +39,7 @@ private:
|
||||
void showTabBarContextMenu(const QPoint &pt);
|
||||
void editMsg();
|
||||
void removeMsg();
|
||||
void updateState(const QHash<MessageId, CanData> * msgs = nullptr);
|
||||
void updateState(const std::set<MessageId> *msgs = nullptr);
|
||||
|
||||
MessageId msg_id;
|
||||
QLabel *warning_icon, *warning_label;
|
||||
|
||||
+11
-11
@@ -7,7 +7,6 @@
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "tools/cabana/commands.h"
|
||||
// HistoryLogModel
|
||||
|
||||
QVariant HistoryLogModel::data(const QModelIndex &index, int role) const {
|
||||
const bool show_signals = display_signals_mode && sigs.size() > 0;
|
||||
@@ -17,11 +16,11 @@ QVariant HistoryLogModel::data(const QModelIndex &index, int role) const {
|
||||
return QString::number((m.mono_time / (double)1e9) - can->routeStartTime(), 'f', 2);
|
||||
}
|
||||
int i = index.column() - 1;
|
||||
return show_signals ? QString::number(m.sig_values[i], 'f', sigs[i]->precision) : toHex(m.data);
|
||||
return show_signals ? QString::number(m.sig_values[i], 'f', sigs[i]->precision) : QString();
|
||||
} else if (role == ColorsRole) {
|
||||
return QVariant::fromValue(m.colors);
|
||||
return QVariant::fromValue((void *)(&m.colors));
|
||||
} else if (role == BytesRole) {
|
||||
return m.data;
|
||||
return QVariant::fromValue((void *)(&m.data));
|
||||
} else if (role == Qt::TextAlignmentRole) {
|
||||
return (uint32_t)(Qt::AlignRight | Qt::AlignVCenter);
|
||||
}
|
||||
@@ -123,7 +122,7 @@ void HistoryLogModel::fetchMore(const QModelIndex &parent) {
|
||||
template <class InputIt>
|
||||
std::deque<HistoryLogModel::Message> HistoryLogModel::fetchData(InputIt first, InputIt last, uint64_t min_time) {
|
||||
std::deque<HistoryLogModel::Message> msgs;
|
||||
QVector<double> values(sigs.size());
|
||||
std::vector<double> values(sigs.size());
|
||||
for (; first != last && (*first)->mono_time > min_time; ++first) {
|
||||
const CanEvent *e = *first;
|
||||
for (int i = 0; i < sigs.size(); ++i) {
|
||||
@@ -132,7 +131,7 @@ std::deque<HistoryLogModel::Message> HistoryLogModel::fetchData(InputIt first, I
|
||||
if (!filter_cmp || filter_cmp(values[filter_sig_idx], filter_value)) {
|
||||
auto &m = msgs.emplace_back();
|
||||
m.mono_time = e->mono_time;
|
||||
m.data = QByteArray((const char *)e->dat, e->size);
|
||||
m.data.assign(e->dat, e->dat + e->size);
|
||||
m.sig_values = values;
|
||||
if (msgs.size() >= batch_size && min_time == 0) {
|
||||
return msgs;
|
||||
@@ -146,7 +145,7 @@ std::deque<HistoryLogModel::Message> HistoryLogModel::fetchData(uint64_t from_ti
|
||||
const auto &events = can->events(msg_id);
|
||||
const auto freq = can->lastMessage(msg_id).freq;
|
||||
const bool update_colors = !display_signals_mode || sigs.empty();
|
||||
|
||||
const std::vector<uint8_t> no_mask;
|
||||
const auto speed = can->getSpeed();
|
||||
if (dynamic_mode) {
|
||||
auto first = std::upper_bound(events.rbegin(), events.rend(), from_time, [](uint64_t ts, auto e) {
|
||||
@@ -155,7 +154,7 @@ std::deque<HistoryLogModel::Message> HistoryLogModel::fetchData(uint64_t from_ti
|
||||
auto msgs = fetchData(first, events.rend(), min_time);
|
||||
if (update_colors && (min_time > 0 || messages.empty())) {
|
||||
for (auto it = msgs.rbegin(); it != msgs.rend(); ++it) {
|
||||
hex_colors.compute(msg_id, it->data.data(), it->data.size(), it->mono_time / (double)1e9, speed, nullptr, freq);
|
||||
hex_colors.compute(msg_id, it->data.data(), it->data.size(), it->mono_time / (double)1e9, speed, no_mask, freq);
|
||||
it->colors = hex_colors.colors;
|
||||
}
|
||||
}
|
||||
@@ -166,7 +165,7 @@ std::deque<HistoryLogModel::Message> HistoryLogModel::fetchData(uint64_t from_ti
|
||||
auto msgs = fetchData(first, events.cend(), 0);
|
||||
if (update_colors) {
|
||||
for (auto it = msgs.begin(); it != msgs.end(); ++it) {
|
||||
hex_colors.compute(msg_id, it->data.data(), it->data.size(), it->mono_time / (double)1e9, speed, nullptr, freq);
|
||||
hex_colors.compute(msg_id, it->data.data(), it->data.size(), it->mono_time / (double)1e9, speed, no_mask, freq);
|
||||
it->colors = hex_colors.colors;
|
||||
}
|
||||
}
|
||||
@@ -177,7 +176,7 @@ std::deque<HistoryLogModel::Message> HistoryLogModel::fetchData(uint64_t from_ti
|
||||
// HeaderView
|
||||
|
||||
QSize HeaderView::sectionSizeFromContents(int logicalIndex) const {
|
||||
static QSize time_col_size = fontMetrics().boundingRect({0, 0, 200, 200}, defaultAlignment(), "000000.000").size() + QSize(10, 6);
|
||||
static const QSize time_col_size = fontMetrics().boundingRect({0, 0, 200, 200}, defaultAlignment(), "000000.000").size() + QSize(10, 6);
|
||||
if (logicalIndex == 0) {
|
||||
return time_col_size;
|
||||
} else {
|
||||
@@ -237,10 +236,11 @@ LogsWidget::LogsWidget(QWidget *parent) : QFrame(parent) {
|
||||
main_layout->addWidget(logs = new QTableView(this));
|
||||
logs->setModel(model = new HistoryLogModel(this));
|
||||
delegate = new MessageBytesDelegate(this);
|
||||
logs->setItemDelegateForColumn(1, new MessageBytesDelegate(this));
|
||||
logs->setHorizontalHeader(new HeaderView(Qt::Horizontal, this));
|
||||
logs->horizontalHeader()->setDefaultAlignment(Qt::AlignRight | (Qt::Alignment)Qt::TextWordWrap);
|
||||
logs->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
logs->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
|
||||
logs->verticalHeader()->setDefaultSectionSize(delegate->sizeForBytes(8).height());
|
||||
logs->verticalHeader()->setVisible(false);
|
||||
logs->setFrameShape(QFrame::NoFrame);
|
||||
|
||||
|
||||
@@ -46,9 +46,9 @@ public slots:
|
||||
public:
|
||||
struct Message {
|
||||
uint64_t mono_time = 0;
|
||||
QVector<double> sig_values;
|
||||
QByteArray data;
|
||||
QVector<QColor> colors;
|
||||
std::vector<double> sig_values;
|
||||
std::vector<uint8_t> data;
|
||||
std::vector<QColor> colors;
|
||||
};
|
||||
|
||||
template <class InputIt>
|
||||
|
||||
+24
-34
@@ -9,6 +9,7 @@
|
||||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonObject>
|
||||
#include <QMenuBar>
|
||||
#include <QMessageBox>
|
||||
#include <QResizeEvent>
|
||||
@@ -21,8 +22,10 @@
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/streamselector.h"
|
||||
#include "tools/cabana/tools/findsignal.h"
|
||||
#include "tools/replay/replay.h"
|
||||
|
||||
MainWindow::MainWindow() : QMainWindow() {
|
||||
loadFingerprints();
|
||||
createDockWindows();
|
||||
setCentralWidget(center_widget = new CenterWidget(this));
|
||||
createActions();
|
||||
@@ -55,12 +58,6 @@ MainWindow::MainWindow() : QMainWindow() {
|
||||
qInfo() << QString::fromStdString(msg);
|
||||
});
|
||||
|
||||
// load fingerprints
|
||||
QFile json_file(QApplication::applicationDirPath() + "/dbc/car_fingerprint_to_dbc.json");
|
||||
if (json_file.open(QIODevice::ReadOnly)) {
|
||||
fingerprint_to_dbc = QJsonDocument::fromJson(json_file.readAll());
|
||||
}
|
||||
|
||||
setStyleSheet(QString(R"(QMainWindow::separator {
|
||||
width: %1px; /* when vertical */
|
||||
height: %1px; /* when horizontal */
|
||||
@@ -76,6 +73,16 @@ MainWindow::MainWindow() : QMainWindow() {
|
||||
QObject::connect(StreamNotifier::instance(), &StreamNotifier::streamStarted, this, &MainWindow::streamStarted);
|
||||
}
|
||||
|
||||
void MainWindow::loadFingerprints() {
|
||||
QFile json_file(QApplication::applicationDirPath() + "/dbc/car_fingerprint_to_dbc.json");
|
||||
if (json_file.open(QIODevice::ReadOnly)) {
|
||||
fingerprint_to_dbc = QJsonDocument::fromJson(json_file.readAll());
|
||||
auto dbc_names = fingerprint_to_dbc.object().toVariantMap().values();
|
||||
std::transform(dbc_names.begin(), dbc_names.end(), std::inserter(opendbc_names, opendbc_names.begin()),
|
||||
[](const auto &name) { return name.toString(); });
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::createActions() {
|
||||
// File menu
|
||||
QMenu *file_menu = menuBar()->addMenu(tr("&File"));
|
||||
@@ -84,8 +91,8 @@ void MainWindow::createActions() {
|
||||
close_stream_act->setEnabled(false);
|
||||
file_menu->addSeparator();
|
||||
|
||||
file_menu->addAction(tr("New DBC File"), [this]() { newFile(); })->setShortcuts(QKeySequence::New);
|
||||
file_menu->addAction(tr("Open DBC File..."), [this]() { openFile(); })->setShortcuts(QKeySequence::Open);
|
||||
file_menu->addAction(tr("New DBC File"), [this]() { newFile(); }, QKeySequence::New);
|
||||
file_menu->addAction(tr("Open DBC File..."), [this]() { openFile(); }, QKeySequence::Open);
|
||||
|
||||
manage_dbcs_menu = file_menu->addMenu(tr("Manage &DBC Files"));
|
||||
|
||||
@@ -101,29 +108,22 @@ void MainWindow::createActions() {
|
||||
file_menu->addSeparator();
|
||||
QMenu *load_opendbc_menu = file_menu->addMenu(tr("Load DBC from commaai/opendbc"));
|
||||
// load_opendbc_menu->setStyleSheet("QMenu { menu-scrollable: true; }");
|
||||
auto dbc_names = allDBCNames();
|
||||
std::sort(dbc_names.begin(), dbc_names.end());
|
||||
for (const auto &name : dbc_names) {
|
||||
QString dbc_name = QString::fromStdString(name);
|
||||
load_opendbc_menu->addAction(dbc_name, [=]() { loadDBCFromOpendbc(dbc_name); });
|
||||
for (const auto &dbc_name : opendbc_names) {
|
||||
load_opendbc_menu->addAction(dbc_name, [this, name = dbc_name]() { loadDBCFromOpendbc(name); });
|
||||
}
|
||||
|
||||
file_menu->addAction(tr("Load DBC From Clipboard"), [=]() { loadFromClipboard(); });
|
||||
|
||||
file_menu->addSeparator();
|
||||
save_dbc = file_menu->addAction(tr("Save DBC..."), this, &MainWindow::save);
|
||||
save_dbc->setShortcuts(QKeySequence::Save);
|
||||
|
||||
save_dbc_as = file_menu->addAction(tr("Save DBC As..."), this, &MainWindow::saveAs);
|
||||
save_dbc_as->setShortcuts(QKeySequence::SaveAs);
|
||||
|
||||
save_dbc = file_menu->addAction(tr("Save DBC..."), this, &MainWindow::save, QKeySequence::Save);
|
||||
save_dbc_as = file_menu->addAction(tr("Save DBC As..."), this, &MainWindow::saveAs, QKeySequence::SaveAs);
|
||||
copy_dbc_to_clipboard = file_menu->addAction(tr("Copy DBC To Clipboard"), this, &MainWindow::saveToClipboard);
|
||||
|
||||
file_menu->addSeparator();
|
||||
file_menu->addAction(tr("Settings..."), this, &MainWindow::setOption)->setShortcuts(QKeySequence::Preferences);
|
||||
file_menu->addAction(tr("Settings..."), this, &MainWindow::setOption, QKeySequence::Preferences);
|
||||
|
||||
file_menu->addSeparator();
|
||||
file_menu->addAction(tr("E&xit"), qApp, &QApplication::closeAllWindows)->setShortcuts(QKeySequence::Quit);
|
||||
file_menu->addAction(tr("E&xit"), qApp, &QApplication::closeAllWindows, QKeySequence::Quit);
|
||||
|
||||
// Edit Menu
|
||||
QMenu *edit_menu = menuBar()->addMenu(tr("&Edit"));
|
||||
@@ -157,7 +157,7 @@ void MainWindow::createActions() {
|
||||
|
||||
// Help Menu
|
||||
QMenu *help_menu = menuBar()->addMenu(tr("&Help"));
|
||||
help_menu->addAction(tr("Help"), this, &MainWindow::onlineHelp)->setShortcuts(QKeySequence::HelpContents);
|
||||
help_menu->addAction(tr("Help"), this, &MainWindow::onlineHelp, QKeySequence::HelpContents);
|
||||
help_menu->addAction(tr("About &Qt"), qApp, &QApplication::aboutQt);
|
||||
}
|
||||
|
||||
@@ -374,7 +374,7 @@ void MainWindow::eventsMerged() {
|
||||
auto dbc_name = fingerprint_to_dbc[car_fingerprint];
|
||||
if (dbc_name != QJsonValue::Undefined) {
|
||||
// Prevent dialog that load autosaved file from blocking replay->start().
|
||||
QTimer::singleShot(0, [dbc_name, this]() { loadDBCFromOpendbc(dbc_name.toString()); });
|
||||
QTimer::singleShot(0, this, [dbc_name, this]() { loadDBCFromOpendbc(dbc_name.toString()); });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -471,11 +471,7 @@ void MainWindow::saveFileToClipboard(DBCFile *dbc_file) {
|
||||
|
||||
void MainWindow::updateLoadSaveMenus() {
|
||||
int cnt = dbc()->nonEmptyDBCCount();
|
||||
if (cnt > 1) {
|
||||
save_dbc->setText(tr("Save %1 DBCs...").arg(dbc()->dbcCount()));
|
||||
} else {
|
||||
save_dbc->setText(tr("Save DBC..."));
|
||||
}
|
||||
save_dbc->setText(cnt > 1 ? tr("Save %1 DBCs...").arg(cnt) : tr("Save DBC..."));
|
||||
save_dbc->setEnabled(cnt > 0);
|
||||
save_dbc_as->setEnabled(cnt == 1);
|
||||
|
||||
@@ -608,12 +604,6 @@ void MainWindow::closeEvent(QCloseEvent *event) {
|
||||
}
|
||||
settings.message_header_state = messages_widget->saveHeaderState();
|
||||
|
||||
auto status = settings.save();
|
||||
if (status == QSettings::AccessError) {
|
||||
QString error = tr("Failed to write settings to [%1]: access denied").arg(Settings::filePath());
|
||||
qDebug() << error;
|
||||
QMessageBox::warning(this, tr("Failed to write settings"), error);
|
||||
}
|
||||
QWidget::closeEvent(event);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QProgressBar>
|
||||
#include <QSplitter>
|
||||
#include <QStatusBar>
|
||||
#include <set>
|
||||
|
||||
#include "tools/cabana/chart/chartswidget.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
@@ -50,6 +51,7 @@ protected:
|
||||
void saveFile(DBCFile *dbc_file);
|
||||
void saveFileAs(DBCFile *dbc_file);
|
||||
void saveFileToClipboard(DBCFile *dbc_file);
|
||||
void loadFingerprints();
|
||||
void loadFromClipboard(SourceSet s = SOURCE_ALL, bool close_all = true);
|
||||
void autoSave();
|
||||
void cleanupAutoSaveFile();
|
||||
@@ -84,6 +86,7 @@ protected:
|
||||
QProgressBar *progress_bar;
|
||||
QLabel *status_label;
|
||||
QJsonDocument fingerprint_to_dbc;
|
||||
std::set<QString> opendbc_names;
|
||||
QSplitter *video_splitter = nullptr;
|
||||
enum { MAX_RECENT_FILES = 15 };
|
||||
QAction *recent_files_acts[MAX_RECENT_FILES] = {};
|
||||
|
||||
+108
-148
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
@@ -10,11 +11,6 @@
|
||||
|
||||
#include "tools/cabana/commands.h"
|
||||
|
||||
static QString msg_node_from_id(const MessageId &id) {
|
||||
auto msg = dbc()->msg(id);
|
||||
return msg ? msg->transmitter : QString();
|
||||
}
|
||||
|
||||
MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
main_layout->setContentsMargins(0, 0, 0, 0);
|
||||
@@ -22,12 +18,10 @@ MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget
|
||||
// toolbar
|
||||
main_layout->addWidget(createToolBar());
|
||||
// message table
|
||||
view = new MessageView(this);
|
||||
model = new MessageListModel(this);
|
||||
header = new MessageViewHeader(this);
|
||||
view->setItemDelegate(delegate = new MessageBytesDelegate(view, settings.multiple_lines_bytes));
|
||||
view->setHeader(header);
|
||||
view->setModel(model);
|
||||
main_layout->addWidget(view = new MessageView(this));
|
||||
view->setItemDelegate(delegate = new MessageBytesDelegate(view, settings.multiple_lines_hex));
|
||||
view->setModel(model = new MessageListModel(this));
|
||||
view->setHeader(header = new MessageViewHeader(this));
|
||||
view->setSortingEnabled(true);
|
||||
view->sortByColumn(MessageListModel::Column::NAME, Qt::AscendingOrder);
|
||||
view->setAllColumnsShowFocus(true);
|
||||
@@ -35,6 +29,7 @@ MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget
|
||||
view->setItemsExpandable(false);
|
||||
view->setIndentation(0);
|
||||
view->setRootIsDecorated(false);
|
||||
view->setUniformRowHeights(!settings.multiple_lines_hex);
|
||||
|
||||
// Must be called before setting any header parameters to avoid overriding
|
||||
restoreHeaderState(settings.message_header_state);
|
||||
@@ -43,15 +38,14 @@ MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget
|
||||
header->setStretchLastSection(true);
|
||||
header->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
|
||||
main_layout->addWidget(view);
|
||||
|
||||
// suppress
|
||||
QHBoxLayout *suppress_layout = new QHBoxLayout();
|
||||
suppress_add = new QPushButton("Suppress Highlighted");
|
||||
suppress_clear = new QPushButton();
|
||||
suppress_layout->addWidget(suppress_add);
|
||||
suppress_layout->addWidget(suppress_clear);
|
||||
QCheckBox *suppress_defined_signals = new QCheckBox(tr("Suppress Defined Signals"), this);
|
||||
suppress_layout->addWidget(suppress_add = new QPushButton("Suppress Highlighted"));
|
||||
suppress_layout->addWidget(suppress_clear = new QPushButton());
|
||||
suppress_clear->setToolTip(tr("Clear suppressed"));
|
||||
suppress_layout->addStretch(1);
|
||||
QCheckBox *suppress_defined_signals = new QCheckBox(tr("Suppress Signals"), this);
|
||||
suppress_defined_signals->setToolTip(tr("Suppress defined signals"));
|
||||
suppress_defined_signals->setChecked(settings.suppress_defined_signals);
|
||||
suppress_layout->addWidget(suppress_defined_signals);
|
||||
main_layout->addLayout(suppress_layout);
|
||||
@@ -61,10 +55,7 @@ MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget
|
||||
QObject::connect(header, &MessageViewHeader::filtersUpdated, model, &MessageListModel::setFilterStrings);
|
||||
QObject::connect(header, &MessageViewHeader::customContextMenuRequested, this, &MessagesWidget::headerContextMenuEvent);
|
||||
QObject::connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, header, &MessageViewHeader::updateHeaderPositions);
|
||||
QObject::connect(suppress_defined_signals, &QCheckBox::stateChanged, [=](int state) {
|
||||
settings.suppress_defined_signals = (state == Qt::Checked);
|
||||
emit settings.changed();
|
||||
});
|
||||
QObject::connect(suppress_defined_signals, &QCheckBox::stateChanged, can, &AbstractStream::suppressDefinedSignals);
|
||||
QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived);
|
||||
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &MessagesWidget::dbcModified);
|
||||
QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &MessagesWidget::dbcModified);
|
||||
@@ -75,24 +66,17 @@ MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget
|
||||
view->updateBytesSectionSize();
|
||||
});
|
||||
QObject::connect(view->selectionModel(), &QItemSelectionModel::currentChanged, [=](const QModelIndex ¤t, const QModelIndex &previous) {
|
||||
if (current.isValid() && current.row() < model->msgs.size()) {
|
||||
auto &id = model->msgs[current.row()];
|
||||
if (current.isValid() && current.row() < model->items_.size()) {
|
||||
const auto &id = model->items_[current.row()].id;
|
||||
if (!current_msg_id || id != *current_msg_id) {
|
||||
current_msg_id = id;
|
||||
emit msgSelectionChanged(*current_msg_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
QObject::connect(suppress_add, &QPushButton::clicked, [=]() {
|
||||
model->suppress();
|
||||
updateSuppressedButtons();
|
||||
});
|
||||
QObject::connect(suppress_clear, &QPushButton::clicked, [=]() {
|
||||
model->clearSuppress();
|
||||
updateSuppressedButtons();
|
||||
});
|
||||
|
||||
updateSuppressedButtons();
|
||||
QObject::connect(suppress_add, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted);
|
||||
QObject::connect(suppress_clear, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted);
|
||||
suppressHighlighted();
|
||||
|
||||
setWhatsThis(tr(R"(
|
||||
<b>Message View</b><br/>
|
||||
@@ -125,19 +109,22 @@ void MessagesWidget::dbcModified() {
|
||||
}
|
||||
|
||||
void MessagesWidget::selectMessage(const MessageId &msg_id) {
|
||||
auto it = std::find(model->msgs.cbegin(), model->msgs.cend(), msg_id);
|
||||
if (it != model->msgs.cend()) {
|
||||
view->setCurrentIndex(model->index(std::distance(model->msgs.cbegin(), it), 0));
|
||||
auto it = std::find_if(model->items_.cbegin(), model->items_.cend(),
|
||||
[&msg_id](auto &item) { return item.id == msg_id; });
|
||||
if (it != model->items_.cend()) {
|
||||
view->setCurrentIndex(model->index(std::distance(model->items_.cbegin(), it), 0));
|
||||
}
|
||||
}
|
||||
|
||||
void MessagesWidget::updateSuppressedButtons() {
|
||||
if (model->suppressed_bytes.empty()) {
|
||||
suppress_clear->setEnabled(false);
|
||||
suppress_clear->setText("Clear Suppressed");
|
||||
} else {
|
||||
void MessagesWidget::suppressHighlighted() {
|
||||
if (sender() == suppress_add) {
|
||||
size_t n = can->suppressHighlighted();
|
||||
suppress_clear->setText(tr("Clear (%1)").arg(n));
|
||||
suppress_clear->setEnabled(true);
|
||||
suppress_clear->setText(QString("Clear Suppressed (%1)").arg(model->suppressed_bytes.size()));
|
||||
} else {
|
||||
can->clearSuppressed();
|
||||
suppress_clear->setText(tr("Clear"));
|
||||
suppress_clear->setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,12 +146,13 @@ void MessagesWidget::menuAboutToShow() {
|
||||
menu->addSeparator();
|
||||
auto action = menu->addAction(tr("Mutlti-Line bytes"), this, &MessagesWidget::setMultiLineBytes);
|
||||
action->setCheckable(true);
|
||||
action->setChecked(settings.multiple_lines_bytes);
|
||||
action->setChecked(settings.multiple_lines_hex);
|
||||
}
|
||||
|
||||
void MessagesWidget::setMultiLineBytes(bool multi) {
|
||||
settings.multiple_lines_bytes = multi;
|
||||
settings.multiple_lines_hex = multi;
|
||||
delegate->setMultipleLines(multi);
|
||||
view->setUniformRowHeights(!multi);
|
||||
view->updateBytesSectionSize();
|
||||
view->doItemsLayout();
|
||||
}
|
||||
@@ -187,7 +175,7 @@ QVariant MessageListModel::headerData(int section, Qt::Orientation orientation,
|
||||
}
|
||||
|
||||
QVariant MessageListModel::data(const QModelIndex &index, int role) const {
|
||||
if (!index.isValid() || index.row() >= msgs.size()) return {};
|
||||
if (!index.isValid() || index.row() >= items_.size()) return {};
|
||||
|
||||
auto getFreq = [](const CanData &d) {
|
||||
if (d.freq > 0 && (can->currentSec() - d.ts - 1.0 / settings.fps) < (5.0 / d.freq)) {
|
||||
@@ -197,33 +185,25 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const {
|
||||
}
|
||||
};
|
||||
|
||||
const auto &id = msgs[index.row()];
|
||||
auto &can_data = can->lastMessage(id);
|
||||
const auto &item = items_[index.row()];
|
||||
const auto &data = can->lastMessage(item.id);
|
||||
if (role == Qt::DisplayRole) {
|
||||
switch (index.column()) {
|
||||
case Column::NAME: return msgName(id);
|
||||
case Column::SOURCE: return id.source != INVALID_SOURCE ? QString::number(id.source) : "N/A";
|
||||
case Column::ADDRESS: return QString::number(id.address, 16);
|
||||
case Column::NODE: return msg_node_from_id(id);
|
||||
case Column::FREQ: return id.source != INVALID_SOURCE ? getFreq(can_data) : "N/A";
|
||||
case Column::COUNT: return id.source != INVALID_SOURCE ? QString::number(can_data.count) : "N/A";
|
||||
case Column::DATA: return id.source != INVALID_SOURCE ? toHex(can_data.dat) : "N/A";
|
||||
case Column::NAME: return item.name;
|
||||
case Column::SOURCE: return item.id.source != INVALID_SOURCE ? QString::number(item.id.source) : "N/A";
|
||||
case Column::ADDRESS: return QString::number(item.id.address, 16);
|
||||
case Column::NODE: return item.node;
|
||||
case Column::FREQ: return item.id.source != INVALID_SOURCE ? getFreq(data) : "N/A";
|
||||
case Column::COUNT: return item.id.source != INVALID_SOURCE ? QString::number(data.count) : "N/A";
|
||||
case Column::DATA: return item.id.source != INVALID_SOURCE ? "" : "N/A";
|
||||
}
|
||||
} else if (role == ColorsRole) {
|
||||
QVector<QColor> colors = can_data.colors;
|
||||
if (!suppressed_bytes.empty()) {
|
||||
for (int i = 0; i < colors.size(); i++) {
|
||||
if (suppressed_bytes.contains({id, i})) {
|
||||
colors[i] = QColor(255, 255, 255, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return QVariant::fromValue(colors);
|
||||
} else if (role == BytesRole && index.column() == Column::DATA && id.source != INVALID_SOURCE) {
|
||||
return can_data.dat;
|
||||
return QVariant::fromValue((void*)(&data.colors));
|
||||
} else if (role == BytesRole && index.column() == Column::DATA && item.id.source != INVALID_SOURCE) {
|
||||
return QVariant::fromValue((void*)(&data.dat));
|
||||
} else if (role == Qt::ToolTipRole && index.column() == Column::NAME) {
|
||||
auto msg = dbc()->msg(id);
|
||||
auto tooltip = msg ? msg->name : UNTITLED;
|
||||
auto msg = dbc()->msg(item.id);
|
||||
auto tooltip = item.name;
|
||||
if (msg && !msg->comment.isEmpty()) tooltip += "<br /><span style=\"color:gray;\">" + msg->comment + "</span>";
|
||||
return tooltip;
|
||||
}
|
||||
@@ -231,31 +211,31 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const {
|
||||
}
|
||||
|
||||
void MessageListModel::setFilterStrings(const QMap<int, QString> &filters) {
|
||||
filter_str = filters;
|
||||
filters_ = filters;
|
||||
filterAndSort();
|
||||
}
|
||||
|
||||
void MessageListModel::dbcModified() {
|
||||
dbc_address.clear();
|
||||
dbc_messages_.clear();
|
||||
for (const auto &[_, m] : dbc()->getMessages(-1)) {
|
||||
dbc_address.insert(m.address);
|
||||
dbc_messages_.insert(MessageId{.source = INVALID_SOURCE, .address = m.address});
|
||||
}
|
||||
filterAndSort();
|
||||
}
|
||||
|
||||
void MessageListModel::sortMessages(std::vector<MessageId> &new_msgs) {
|
||||
auto do_sort = [order = sort_order](std::vector<MessageId> &m, auto proj) {
|
||||
std::sort(m.begin(), m.end(), [order, proj = std::move(proj)](auto &l, auto &r) {
|
||||
void MessageListModel::sortItems(std::vector<MessageListModel::Item> &items) {
|
||||
auto do_sort = [order = sort_order](std::vector<MessageListModel::Item> &m, auto proj) {
|
||||
std::stable_sort(m.begin(), m.end(), [order, proj = std::move(proj)](auto &l, auto &r) {
|
||||
return order == Qt::AscendingOrder ? proj(l) < proj(r) : proj(l) > proj(r);
|
||||
});
|
||||
};
|
||||
switch (sort_column) {
|
||||
case Column::NAME: do_sort(new_msgs, [](auto &id) { return std::make_pair(msgName(id), id); }); break;
|
||||
case Column::SOURCE: do_sort(new_msgs, [](auto &id) { return std::tie(id.source, id); }); break;
|
||||
case Column::ADDRESS: do_sort(new_msgs, [](auto &id) { return std::tie(id.address, id);}); break;
|
||||
case Column::NODE: do_sort(new_msgs, [](auto &id) { return std::make_pair(msg_node_from_id(id), id);}); break;
|
||||
case Column::FREQ: do_sort(new_msgs, [](auto &id) { return std::tie(can->lastMessage(id).freq, id); }); break;
|
||||
case Column::COUNT: do_sort(new_msgs, [](auto &id) { return std::tie(can->lastMessage(id).count, id); }); break;
|
||||
case Column::NAME: do_sort(items, [](auto &item) { return std::tie(item.name, item.id); }); break;
|
||||
case Column::SOURCE: do_sort(items, [](auto &item) { return std::tie(item.id.source, item.id); }); break;
|
||||
case Column::ADDRESS: do_sort(items, [](auto &item) { return std::tie(item.id.address, item.id);}); break;
|
||||
case Column::NODE: do_sort(items, [](auto &item) { return std::tie(item.node, item.id);}); break;
|
||||
case Column::FREQ: do_sort(items, [](auto &item) { return std::make_pair(can->lastMessage(item.id).freq, item.id); }); break;
|
||||
case Column::COUNT: do_sort(items, [](auto &item) { return std::make_pair(can->lastMessage(item.id).count, item.id); }); break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,29 +254,33 @@ static bool parseRange(const QString &filter, uint32_t value, int base = 10) {
|
||||
return ok && value >= min && value <= max;
|
||||
}
|
||||
|
||||
bool MessageListModel::matchMessage(const MessageId &id, const CanData &data, const QMap<int, QString> &filters) {
|
||||
bool MessageListModel::match(const MessageListModel::Item &item) {
|
||||
if (filters_.isEmpty())
|
||||
return true;
|
||||
|
||||
bool match = true;
|
||||
for (auto it = filters.cbegin(); it != filters.cend() && match; ++it) {
|
||||
const auto &data = can->lastMessage(item.id);
|
||||
for (auto it = filters_.cbegin(); it != filters_.cend() && match; ++it) {
|
||||
const QString &txt = it.value();
|
||||
QRegularExpression re(txt, QRegularExpression::CaseInsensitiveOption | QRegularExpression::DotMatchesEverythingOption);
|
||||
switch (it.key()) {
|
||||
case Column::NAME: {
|
||||
const auto msg = dbc()->msg(id);
|
||||
match = re.match(msg ? msg->name : UNTITLED).hasMatch();
|
||||
match = match || (msg && std::any_of(msg->sigs.cbegin(), msg->sigs.cend(),
|
||||
[&re](const auto &s) { return re.match(s->name).hasMatch(); }));
|
||||
match = item.name.contains(txt, Qt::CaseInsensitive);
|
||||
if (!match) {
|
||||
const auto m = dbc()->msg(item.id);
|
||||
match = m && std::any_of(m->sigs.cbegin(), m->sigs.cend(),
|
||||
[&txt](const auto &s) { return s->name.contains(txt, Qt::CaseInsensitive); });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Column::SOURCE:
|
||||
match = parseRange(txt, id.source);
|
||||
match = parseRange(txt, item.id.source);
|
||||
break;
|
||||
case Column::ADDRESS: {
|
||||
match = re.match(QString::number(id.address, 16)).hasMatch();
|
||||
match = match || parseRange(txt, id.address, 16);
|
||||
case Column::ADDRESS:
|
||||
match = QString::number(item.id.address, 16).contains(txt, Qt::CaseInsensitive);
|
||||
match = match || parseRange(txt, item.id.address, 16);
|
||||
break;
|
||||
}
|
||||
case Column::NODE:
|
||||
match = re.match(msg_node_from_id(id)).hasMatch();
|
||||
match = item.node.contains(txt, Qt::CaseInsensitive);
|
||||
break;
|
||||
case Column::FREQ:
|
||||
// TODO: Hide stale messages?
|
||||
@@ -305,52 +289,50 @@ bool MessageListModel::matchMessage(const MessageId &id, const CanData &data, co
|
||||
case Column::COUNT:
|
||||
match = parseRange(txt, data.count);
|
||||
break;
|
||||
case Column::DATA: {
|
||||
match = QString(data.dat.toHex()).contains(txt, Qt::CaseInsensitive);
|
||||
match = match || re.match(QString(data.dat.toHex())).hasMatch();
|
||||
match = match || re.match(QString(data.dat.toHex(' '))).hasMatch();
|
||||
case Column::DATA:
|
||||
match = utils::toHex(data.dat).contains(txt, Qt::CaseInsensitive);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
void MessageListModel::filterAndSort() {
|
||||
std::vector<MessageId> new_msgs;
|
||||
new_msgs.reserve(can->last_msgs.size() + dbc_address.size());
|
||||
|
||||
auto address = dbc_address;
|
||||
for (auto it = can->last_msgs.cbegin(); it != can->last_msgs.cend(); ++it) {
|
||||
if (filter_str.isEmpty() || matchMessage(it.key(), it.value(), filter_str)) {
|
||||
new_msgs.push_back(it.key());
|
||||
}
|
||||
address.remove(it.key().address);
|
||||
// merge CAN and DBC messages
|
||||
std::vector<MessageId> all_messages;
|
||||
all_messages.reserve(can->lastMessages().size() + dbc_messages_.size());
|
||||
auto dbc_msgs = dbc_messages_;
|
||||
for (const auto &[id, m] : can->lastMessages()) {
|
||||
all_messages.push_back(id);
|
||||
dbc_msgs.erase(MessageId{.source = INVALID_SOURCE, .address = id.address});
|
||||
}
|
||||
all_messages.insert(all_messages.end(), dbc_msgs.begin(), dbc_msgs.end());
|
||||
|
||||
// merge all DBC messages
|
||||
for (auto &addr : address) {
|
||||
MessageId id{.source = INVALID_SOURCE, .address = addr};
|
||||
if (filter_str.isEmpty() || matchMessage(id, {}, filter_str)) {
|
||||
new_msgs.push_back(id);
|
||||
}
|
||||
// filter and sort
|
||||
std::vector<Item> items;
|
||||
for (const auto &id : all_messages) {
|
||||
auto msg = dbc()->msg(id);
|
||||
Item item = {.id = id,
|
||||
.name = msg ? msg->name : UNTITLED,
|
||||
.node = msg ? msg->transmitter : QString()};
|
||||
if (match(item))
|
||||
items.emplace_back(item);
|
||||
}
|
||||
sortItems(items);
|
||||
|
||||
sortMessages(new_msgs);
|
||||
|
||||
if (msgs != new_msgs) {
|
||||
if (items_ != items) {
|
||||
beginResetModel();
|
||||
msgs = std::move(new_msgs);
|
||||
items_ = std::move(items);
|
||||
endResetModel();
|
||||
}
|
||||
}
|
||||
|
||||
void MessageListModel::msgsReceived(const QHash<MessageId, CanData> *new_msgs, bool has_new_ids) {
|
||||
if (has_new_ids || filter_str.contains(Column::FREQ) || filter_str.contains(Column::COUNT) || filter_str.contains(Column::DATA)) {
|
||||
void MessageListModel::msgsReceived(const std::set<MessageId> *new_msgs, bool has_new_ids) {
|
||||
if (has_new_ids || filters_.contains(Column::FREQ) || filters_.contains(Column::COUNT) || filters_.contains(Column::DATA)) {
|
||||
filterAndSort();
|
||||
}
|
||||
for (int i = 0; i < msgs.size(); ++i) {
|
||||
if (new_msgs->contains(msgs[i])) {
|
||||
for (int i = 0; i < items_.size(); ++i) {
|
||||
if (!new_msgs || new_msgs->count(items_[i].id)) {
|
||||
for (int col = Column::FREQ; col < columnCount(); ++col)
|
||||
emit dataChanged(index(i, col), index(i, col), {Qt::DisplayRole});
|
||||
}
|
||||
@@ -358,31 +340,13 @@ void MessageListModel::msgsReceived(const QHash<MessageId, CanData> *new_msgs, b
|
||||
}
|
||||
|
||||
void MessageListModel::sort(int column, Qt::SortOrder order) {
|
||||
if (column != columnCount() - 1) {
|
||||
if (column != Column::DATA) {
|
||||
sort_column = column;
|
||||
sort_order = order;
|
||||
filterAndSort();
|
||||
}
|
||||
}
|
||||
|
||||
void MessageListModel::suppress() {
|
||||
const double cur_ts = can->currentSec();
|
||||
|
||||
for (auto &id : msgs) {
|
||||
auto &can_data = can->lastMessage(id);
|
||||
for (int i = 0; i < can_data.dat.size(); i++) {
|
||||
const double dt = cur_ts - can_data.last_change_t[i];
|
||||
if (dt < 2.0) {
|
||||
suppressed_bytes.insert({id, i});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MessageListModel::clearSuppress() {
|
||||
suppressed_bytes.clear();
|
||||
}
|
||||
|
||||
// MessageView
|
||||
|
||||
void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
|
||||
@@ -413,14 +377,11 @@ void MessageView::updateBytesSectionSize() {
|
||||
auto delegate = ((MessageBytesDelegate *)itemDelegate());
|
||||
int max_bytes = 8;
|
||||
if (!delegate->multipleLines()) {
|
||||
for (auto it = can->last_msgs.constBegin(); it != can->last_msgs.constEnd(); ++it) {
|
||||
max_bytes = std::max(max_bytes, it.value().dat.size());
|
||||
for (const auto &[_, m] : can->lastMessages()) {
|
||||
max_bytes = std::max<int>(max_bytes, m.dat.size());
|
||||
}
|
||||
}
|
||||
int width = delegate->widthForBytes(max_bytes);
|
||||
if (header()->sectionSize(MessageListModel::Column::DATA) != width) {
|
||||
header()->resizeSection(MessageListModel::Column::DATA, width);
|
||||
}
|
||||
header()->resizeSection(MessageListModel::Column::DATA, delegate->sizeForBytes(max_bytes).width());
|
||||
}
|
||||
|
||||
// MessageViewHeader
|
||||
@@ -445,8 +406,7 @@ void MessageViewHeader::updateHeaderPositions() {
|
||||
for (int i = 0; i < count(); i++) {
|
||||
if (editors[i]) {
|
||||
int h = editors[i]->sizeHint().height();
|
||||
editors[i]->move(sectionViewportPosition(i), sz.height());
|
||||
editors[i]->resize(sectionSize(i), h);
|
||||
editors[i]->setGeometry(sectionViewportPosition(i), sz.height(), sectionSize(i), h);
|
||||
editors[i]->setHidden(isSectionHidden(i));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -9,7 +11,6 @@
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QSet>
|
||||
#include <QToolBar>
|
||||
#include <QTreeView>
|
||||
|
||||
@@ -34,23 +35,29 @@ public:
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return Column::DATA + 1; }
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override { return msgs.size(); }
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override { return items_.size(); }
|
||||
void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override;
|
||||
void setFilterStrings(const QMap<int, QString> &filters);
|
||||
void msgsReceived(const QHash<MessageId, CanData> *new_msgs, bool has_new_ids);
|
||||
void msgsReceived(const std::set<MessageId> *new_msgs, bool has_new_ids);
|
||||
void filterAndSort();
|
||||
void suppress();
|
||||
void clearSuppress();
|
||||
void dbcModified();
|
||||
std::vector<MessageId> msgs;
|
||||
QSet<std::pair<MessageId, int>> suppressed_bytes;
|
||||
|
||||
struct Item {
|
||||
MessageId id;
|
||||
QString name;
|
||||
QString node;
|
||||
bool operator==(const Item &other) const {
|
||||
return id == other.id && name == other.name && node == other.node;
|
||||
}
|
||||
};
|
||||
std::vector<Item> items_;
|
||||
|
||||
private:
|
||||
void sortMessages(std::vector<MessageId> &new_msgs);
|
||||
bool matchMessage(const MessageId &id, const CanData &data, const QMap<int, QString> &filters);
|
||||
void sortItems(std::vector<MessageListModel::Item> &items);
|
||||
bool match(const MessageListModel::Item &id);
|
||||
|
||||
QMap<int, QString> filter_str;
|
||||
QSet<uint32_t> dbc_address;
|
||||
QMap<int, QString> filters_;
|
||||
std::set<MessageId> dbc_messages_;
|
||||
int sort_column = 0;
|
||||
Qt::SortOrder sort_order = Qt::AscendingOrder;
|
||||
};
|
||||
@@ -91,7 +98,7 @@ public:
|
||||
void selectMessage(const MessageId &message_id);
|
||||
QByteArray saveHeaderState() const { return view->header()->saveState(); }
|
||||
bool restoreHeaderState(const QByteArray &state) const { return view->header()->restoreState(state); }
|
||||
void updateSuppressedButtons();
|
||||
void suppressHighlighted();
|
||||
|
||||
public slots:
|
||||
void dbcModified();
|
||||
|
||||
+49
-79
@@ -6,65 +6,51 @@
|
||||
#include <QFileDialog>
|
||||
#include <QFormLayout>
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
#include <type_traits>
|
||||
|
||||
#include "tools/cabana/util.h"
|
||||
|
||||
Settings settings;
|
||||
|
||||
QSettings::Status Settings::save() {
|
||||
QSettings s(filePath(), QSettings::IniFormat);
|
||||
s.setValue("absolute_time", absolute_time);
|
||||
s.setValue("fps", fps);
|
||||
s.setValue("max_cached_minutes", max_cached_minutes);
|
||||
s.setValue("chart_height", chart_height);
|
||||
s.setValue("chart_range", chart_range);
|
||||
s.setValue("chart_column_count", chart_column_count);
|
||||
s.setValue("last_dir", last_dir);
|
||||
s.setValue("last_route_dir", last_route_dir);
|
||||
s.setValue("window_state", window_state);
|
||||
s.setValue("geometry", geometry);
|
||||
s.setValue("video_splitter_state", video_splitter_state);
|
||||
s.setValue("recent_files", recent_files);
|
||||
s.setValue("message_header_state_v3", message_header_state);
|
||||
s.setValue("chart_series_type", chart_series_type);
|
||||
s.setValue("theme", theme);
|
||||
s.setValue("sparkline_range", sparkline_range);
|
||||
s.setValue("multiple_lines_bytes", multiple_lines_bytes);
|
||||
s.setValue("log_livestream", log_livestream);
|
||||
s.setValue("log_path", log_path);
|
||||
s.setValue("drag_direction", drag_direction);
|
||||
s.setValue("suppress_defined_signals", suppress_defined_signals);
|
||||
s.sync();
|
||||
return s.status();
|
||||
template <class SettingOperation>
|
||||
void settings_op(SettingOperation op) {
|
||||
QSettings s("cabana");
|
||||
op(s, "absolute_time", settings.absolute_time);
|
||||
op(s, "fps", settings.fps);
|
||||
op(s, "max_cached_minutes", settings.max_cached_minutes);
|
||||
op(s, "chart_height", settings.chart_height);
|
||||
op(s, "chart_range", settings.chart_range);
|
||||
op(s, "chart_column_count", settings.chart_column_count);
|
||||
op(s, "last_dir", settings.last_dir);
|
||||
op(s, "last_route_dir", settings.last_route_dir);
|
||||
op(s, "window_state", settings.window_state);
|
||||
op(s, "geometry", settings.geometry);
|
||||
op(s, "video_splitter_state", settings.video_splitter_state);
|
||||
op(s, "recent_files", settings.recent_files);
|
||||
op(s, "message_header_state", settings.message_header_state);
|
||||
op(s, "chart_series_type", settings.chart_series_type);
|
||||
op(s, "theme", settings.theme);
|
||||
op(s, "sparkline_range", settings.sparkline_range);
|
||||
op(s, "multiple_lines_hex", settings.multiple_lines_hex);
|
||||
op(s, "log_livestream", settings.log_livestream);
|
||||
op(s, "log_path", settings.log_path);
|
||||
op(s, "drag_direction", (int &)settings.drag_direction);
|
||||
op(s, "suppress_defined_signals", settings.suppress_defined_signals);
|
||||
}
|
||||
|
||||
void Settings::load() {
|
||||
QSettings s(filePath(), QSettings::IniFormat);
|
||||
absolute_time = s.value("absolute_time", false).toBool();
|
||||
fps = s.value("fps", 10).toInt();
|
||||
max_cached_minutes = s.value("max_cached_minutes", 30).toInt();
|
||||
chart_height = s.value("chart_height", 200).toInt();
|
||||
chart_range = s.value("chart_range", 3 * 60).toInt();
|
||||
chart_column_count = s.value("chart_column_count", 1).toInt();
|
||||
last_dir = s.value("last_dir", QDir::homePath()).toString();
|
||||
last_route_dir = s.value("last_route_dir", QDir::homePath()).toString();
|
||||
window_state = s.value("window_state").toByteArray();
|
||||
geometry = s.value("geometry").toByteArray();
|
||||
video_splitter_state = s.value("video_splitter_state").toByteArray();
|
||||
recent_files = s.value("recent_files").toStringList();
|
||||
message_header_state = s.value("message_header_state_v3").toByteArray();
|
||||
chart_series_type = s.value("chart_series_type", 0).toInt();
|
||||
theme = s.value("theme", 0).toInt();
|
||||
sparkline_range = s.value("sparkline_range", 15).toInt();
|
||||
multiple_lines_bytes = s.value("multiple_lines_bytes", true).toBool();
|
||||
log_livestream = s.value("log_livestream", true).toBool();
|
||||
log_path = s.value("log_path").toString();
|
||||
drag_direction = (Settings::DragDirection)s.value("drag_direction", 0).toInt();
|
||||
suppress_defined_signals = s.value("suppress_defined_signals", false).toBool();
|
||||
if (log_path.isEmpty()) {
|
||||
log_path = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/cabana_live_stream/";
|
||||
}
|
||||
Settings::Settings() {
|
||||
last_dir = last_route_dir = QDir::homePath();
|
||||
log_path = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/cabana_live_stream/";
|
||||
settings_op([](QSettings &s, const QString &key, auto &value) {
|
||||
if (auto v = s.value(key); v.canConvert<std::decay_t<decltype(value)>>())
|
||||
value = v.value<std::decay_t<decltype(value)>>();
|
||||
});
|
||||
}
|
||||
|
||||
Settings::~Settings() {
|
||||
settings_op([](QSettings &s, const QString &key, auto &v) { s.setValue(key, v); });
|
||||
}
|
||||
|
||||
// SettingsDlg
|
||||
@@ -75,45 +61,39 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) {
|
||||
QGroupBox *groupbox = new QGroupBox("General");
|
||||
QFormLayout *form_layout = new QFormLayout(groupbox);
|
||||
|
||||
theme = new QComboBox(this);
|
||||
form_layout->addRow(tr("Color Theme"), theme = new QComboBox(this));
|
||||
theme->setToolTip(tr("You may need to restart cabana after changes theme"));
|
||||
theme->addItems({tr("Automatic"), tr("Light"), tr("Dark")});
|
||||
theme->setCurrentIndex(settings.theme);
|
||||
form_layout->addRow(tr("Color Theme"), theme);
|
||||
|
||||
fps = new QSpinBox(this);
|
||||
form_layout->addRow("FPS", fps = new QSpinBox(this));
|
||||
fps->setRange(10, 100);
|
||||
fps->setSingleStep(10);
|
||||
fps->setValue(settings.fps);
|
||||
form_layout->addRow("FPS", fps);
|
||||
|
||||
cached_minutes = new QSpinBox(this);
|
||||
form_layout->addRow(tr("Max Cached Minutes"), cached_minutes = new QSpinBox(this));
|
||||
cached_minutes->setRange(5, 60);
|
||||
cached_minutes->setSingleStep(1);
|
||||
cached_minutes->setValue(settings.max_cached_minutes);
|
||||
form_layout->addRow(tr("Max Cached Minutes"), cached_minutes);
|
||||
main_layout->addWidget(groupbox);
|
||||
|
||||
groupbox = new QGroupBox("New Signal Settings");
|
||||
form_layout = new QFormLayout(groupbox);
|
||||
drag_direction = new QComboBox(this);
|
||||
form_layout->addRow(tr("Drag Direction"), drag_direction = new QComboBox(this));
|
||||
drag_direction->addItems({tr("MSB First"), tr("LSB First"), tr("Always Little Endian"), tr("Always Big Endian")});
|
||||
drag_direction->setCurrentIndex(settings.drag_direction);
|
||||
form_layout->addRow(tr("Drag Direction"), drag_direction);
|
||||
main_layout->addWidget(groupbox);
|
||||
|
||||
groupbox = new QGroupBox("Chart");
|
||||
form_layout = new QFormLayout(groupbox);
|
||||
chart_series_type = new QComboBox(this);
|
||||
form_layout->addRow(tr("Default Series Type"), chart_series_type = new QComboBox(this));
|
||||
chart_series_type->addItems({tr("Line"), tr("Step Line"), tr("Scatter")});
|
||||
chart_series_type->setCurrentIndex(settings.chart_series_type);
|
||||
form_layout->addRow(tr("Chart Default Series Type"), chart_series_type);
|
||||
|
||||
chart_height = new QSpinBox(this);
|
||||
form_layout->addRow(tr("Chart Height"), chart_height = new QSpinBox(this));
|
||||
chart_height->setRange(100, 500);
|
||||
chart_height->setSingleStep(10);
|
||||
chart_height->setValue(settings.chart_height);
|
||||
form_layout->addRow(tr("Chart Height"), chart_height);
|
||||
main_layout->addWidget(groupbox);
|
||||
|
||||
log_livestream = new QGroupBox(tr("Enable live stream logging"), this);
|
||||
@@ -125,10 +105,9 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) {
|
||||
path_layout->addWidget(browse_btn);
|
||||
main_layout->addWidget(log_livestream);
|
||||
|
||||
|
||||
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Apply);
|
||||
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
main_layout->addWidget(buttonBox);
|
||||
main_layout->addStretch(1);
|
||||
setFixedSize(400, sizeHint().height());
|
||||
|
||||
QObject::connect(browse_btn, &QPushButton::clicked, [this]() {
|
||||
QString fn = QFileDialog::getExistingDirectory(
|
||||
@@ -139,31 +118,22 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) {
|
||||
log_path->setText(fn);
|
||||
}
|
||||
});
|
||||
QObject::connect(buttonBox, &QDialogButtonBox::clicked, [=](QAbstractButton *button) {
|
||||
auto role = buttonBox->buttonRole(button);
|
||||
if (role == QDialogButtonBox::AcceptRole) {
|
||||
save();
|
||||
accept();
|
||||
} else if (role == QDialogButtonBox::ApplyRole) {
|
||||
save();
|
||||
} else if (role == QDialogButtonBox::RejectRole) {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDlg::save);
|
||||
}
|
||||
|
||||
void SettingsDlg::save() {
|
||||
settings.fps = fps->value();
|
||||
if (std::exchange(settings.theme, theme->currentIndex()) != settings.theme) {
|
||||
// set theme before emit changed
|
||||
utils::setTheme(settings.theme);
|
||||
}
|
||||
settings.fps = fps->value();
|
||||
settings.max_cached_minutes = cached_minutes->value();
|
||||
settings.chart_series_type = chart_series_type->currentIndex();
|
||||
settings.chart_height = chart_height->value();
|
||||
settings.log_livestream = log_livestream->isChecked();
|
||||
settings.log_path = log_path->text();
|
||||
settings.drag_direction = (Settings::DragDirection)drag_direction->currentIndex();
|
||||
settings.save();
|
||||
emit settings.changed();
|
||||
QDialog::accept();
|
||||
}
|
||||
|
||||
+4
-11
@@ -1,13 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <QApplication>
|
||||
#include <QByteArray>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QGroupBox>
|
||||
#include <QLineEdit>
|
||||
#include <QSettings>
|
||||
#include <QSpinBox>
|
||||
|
||||
#define LIGHT_THEME 1
|
||||
@@ -24,10 +21,8 @@ public:
|
||||
AlwaysBE,
|
||||
};
|
||||
|
||||
Settings() {}
|
||||
QSettings::Status save();
|
||||
void load();
|
||||
inline static QString filePath() { return QApplication::applicationDirPath() + "/settings"; }
|
||||
Settings();
|
||||
~Settings();
|
||||
|
||||
bool absolute_time = false;
|
||||
int fps = 10;
|
||||
@@ -38,7 +33,7 @@ public:
|
||||
int chart_series_type = 0;
|
||||
int theme = 0;
|
||||
int sparkline_range = 15; // 15 seconds
|
||||
bool multiple_lines_bytes = true;
|
||||
bool multiple_lines_hex = false;
|
||||
bool log_livestream = true;
|
||||
bool suppress_defined_signals = false;
|
||||
QString log_path;
|
||||
@@ -49,15 +44,13 @@ public:
|
||||
QByteArray window_state;
|
||||
QStringList recent_files;
|
||||
QByteArray message_header_state;
|
||||
DragDirection drag_direction;
|
||||
DragDirection drag_direction = MsbFirst;
|
||||
|
||||
signals:
|
||||
void changed();
|
||||
};
|
||||
|
||||
class SettingsDlg : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SettingsDlg(QWidget *parent);
|
||||
void save();
|
||||
|
||||
+16
-26
@@ -36,8 +36,8 @@ SignalModel::SignalModel(QObject *parent) : root(new Item), QAbstractItemModel(p
|
||||
void SignalModel::insertItem(SignalModel::Item *parent_item, int pos, const cabana::Signal *sig) {
|
||||
Item *item = new Item{.sig = sig, .parent = parent_item, .title = sig->name, .type = Item::Sig};
|
||||
parent_item->children.insert(pos, item);
|
||||
QString titles[]{"Name", "Size", "Node", "Little Endian", "Signed", "Offset", "Factor", "Type", "Multiplex Value", "Extra Info",
|
||||
"Unit", "Comment", "Minimum Value", "Maximum Value", "Value Descriptions"};
|
||||
QString titles[]{"Name", "Size", "Receiver Nodes", "Little Endian", "Signed", "Offset", "Factor", "Type",
|
||||
"Multiplex Value", "Extra Info", "Unit", "Comment", "Minimum Value", "Maximum Value", "Value Table"};
|
||||
for (int i = 0; i < std::size(titles); ++i) {
|
||||
item->children.push_back(new Item{.sig = sig, .parent = item, .title = titles[i], .type = (Item::Type)(i + Item::Name)});
|
||||
}
|
||||
@@ -68,10 +68,7 @@ void SignalModel::refresh() {
|
||||
}
|
||||
|
||||
SignalModel::Item *SignalModel::getItem(const QModelIndex &index) const {
|
||||
SignalModel::Item *item = nullptr;
|
||||
if (index.isValid()) {
|
||||
item = (SignalModel::Item *)index.internalPointer();
|
||||
}
|
||||
auto item = index.isValid() ? (SignalModel::Item *)index.internalPointer() : nullptr;
|
||||
return item ? item : root.get();
|
||||
}
|
||||
|
||||
@@ -354,7 +351,7 @@ void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op
|
||||
painter->setPen(option.palette.color(option.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text));
|
||||
QRect rect = r.adjusted(sparkline_size.width() + 1, 0, 0, 0);
|
||||
int value_adjust = 10;
|
||||
if (item->highlight || option.state & QStyle::State_Selected) {
|
||||
if (!item->sparkline.isEmpty() && (item->highlight || option.state & QStyle::State_Selected)) {
|
||||
painter->drawLine(rect.topLeft(), rect.bottomLeft());
|
||||
rect.adjust(5, -v_margin, 0, v_margin);
|
||||
painter->setFont(minmax_font);
|
||||
@@ -364,13 +361,12 @@ void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op
|
||||
painter->drawText(rect, Qt::AlignLeft | Qt::AlignBottom, min);
|
||||
QFontMetrics fm(minmax_font);
|
||||
value_adjust = std::max(fm.width(min), fm.width(max)) + 5;
|
||||
} else if (item->sig->type == cabana::Signal::Type::Multiplexed) {
|
||||
} else if (!item->sparkline.isEmpty() && item->sig->type == cabana::Signal::Type::Multiplexed) {
|
||||
// display freq of multiplexed signal
|
||||
painter->setFont(label_font);
|
||||
QString freq = QString("%1 hz").arg(item->sparkline.freq(), 0, 'g', 2);
|
||||
painter->drawText(rect.adjusted(5, 0, 0, 0), Qt::AlignLeft | Qt::AlignVCenter, freq);
|
||||
QFontMetrics fm(label_font);
|
||||
value_adjust = fm.width(freq) + 10;
|
||||
value_adjust = QFontMetrics(label_font).width(freq) + 10;
|
||||
}
|
||||
// signal value
|
||||
painter->setFont(option.font);
|
||||
@@ -504,7 +500,7 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts),
|
||||
QObject::connect(can, &AbstractStream::msgsReceived, this, &SignalView::updateState);
|
||||
QObject::connect(tree->header(), &QHeaderView::sectionResized, [this](int logicalIndex, int oldSize, int newSize) {
|
||||
if (logicalIndex == 1) {
|
||||
value_column_width = newSize - delegate->button_size.width();
|
||||
value_column_width = newSize;
|
||||
updateState();
|
||||
}
|
||||
});
|
||||
@@ -614,21 +610,17 @@ void SignalView::handleSignalAdded(MessageId id, const cabana::Signal *sig) {
|
||||
}
|
||||
|
||||
void SignalView::handleSignalUpdated(const cabana::Signal *sig) {
|
||||
if (int row = model->signalRow(sig); row != -1) {
|
||||
auto item = model->getItem(model->index(row, 1));
|
||||
// invalidate the sparkline
|
||||
item->sparkline.last_ts = 0;
|
||||
if (int row = model->signalRow(sig); row != -1)
|
||||
updateState();
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::updateState(const QHash<MessageId, CanData> *msgs) {
|
||||
void SignalView::updateState(const std::set<MessageId> *msgs) {
|
||||
const auto &last_msg = can->lastMessage(model->msg_id);
|
||||
if (model->rowCount() == 0 || (msgs && !msgs->contains(model->msg_id)) || last_msg.dat.size() == 0) return;
|
||||
if (model->rowCount() == 0 || (msgs && !msgs->count(model->msg_id)) || last_msg.dat.size() == 0) return;
|
||||
|
||||
for (auto item : model->root->children) {
|
||||
double value = 0;
|
||||
if (item->sig->getValue((uint8_t *)last_msg.dat.constData(), last_msg.dat.size(), &value)) {
|
||||
if (item->sig->getValue(last_msg.dat.data(), last_msg.dat.size(), &value)) {
|
||||
item->sig_val = item->sig->formatValue(value);
|
||||
}
|
||||
max_value_width = std::max(max_value_width, fontMetrics().width(item->sig_val));
|
||||
@@ -645,17 +637,15 @@ void SignalView::updateState(const QHash<MessageId, CanData> *msgs) {
|
||||
}
|
||||
|
||||
const static int min_max_width = QFontMetrics(delegate->minmax_font).width("-000.00") + 5;
|
||||
int value_width = std::min<int>(max_value_width + min_max_width, value_column_width / 2);
|
||||
QSize size(value_column_width - value_width,
|
||||
int available_width = value_column_width - delegate->button_size.width();
|
||||
int value_width = std::min<int>(max_value_width + min_max_width, available_width / 2);
|
||||
QSize size(available_width - value_width,
|
||||
delegate->button_size.height() - style()->pixelMetric(QStyle::PM_FocusFrameVMargin) * 2);
|
||||
QFutureSynchronizer<void> synchronizer;
|
||||
for (int i = first_visible_row; i <= last_visible_row; ++i) {
|
||||
auto item = model->getItem(model->index(i, 1));
|
||||
auto &s = item->sparkline;
|
||||
if (s.last_ts != last_msg.ts || s.size() != size || s.time_range != settings.sparkline_range) {
|
||||
synchronizer.addFuture(QtConcurrent::run(
|
||||
&s, &Sparkline::update, model->msg_id, item->sig, last_msg.ts, settings.sparkline_range, size));
|
||||
}
|
||||
synchronizer.addFuture(QtConcurrent::run(
|
||||
&item->sparkline, &Sparkline::update, model->msg_id, item->sig, last_msg.ts, settings.sparkline_range, size));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QLabel>
|
||||
@@ -82,7 +83,7 @@ class SignalItemDelegate : public QStyledItemDelegate {
|
||||
public:
|
||||
SignalItemDelegate(QObject *parent);
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;
|
||||
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
|
||||
@@ -117,7 +118,7 @@ private:
|
||||
void setSparklineRange(int value);
|
||||
void handleSignalAdded(MessageId id, const cabana::Signal *sig);
|
||||
void handleSignalUpdated(const cabana::Signal *sig);
|
||||
void updateState(const QHash<MessageId, CanData> *msgs = nullptr);
|
||||
void updateState(const std::set<MessageId> *msgs = nullptr);
|
||||
|
||||
struct TreeView : public QTreeView {
|
||||
TreeView(QWidget *parent) : QTreeView(parent) {}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
#include <QTimer>
|
||||
#include "common/timing.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
|
||||
static const int EVENT_NEXT_BUFFER_SIZE = 6 * 1024 * 1024; // 6MB
|
||||
|
||||
@@ -15,82 +17,97 @@ StreamNotifier *StreamNotifier::instance() {
|
||||
|
||||
AbstractStream::AbstractStream(QObject *parent) : QObject(parent) {
|
||||
assert(parent != nullptr);
|
||||
new_msgs = std::make_unique<QHash<MessageId, CanData>>();
|
||||
event_buffer = std::make_unique<MonotonicBuffer>(EVENT_NEXT_BUFFER_SIZE);
|
||||
event_buffer_ = std::make_unique<MonotonicBuffer>(EVENT_NEXT_BUFFER_SIZE);
|
||||
|
||||
QObject::connect(this, &AbstractStream::privateUpdateLastMsgsSignal, this, &AbstractStream::updateLastMessages, Qt::QueuedConnection);
|
||||
QObject::connect(this, &AbstractStream::seekedTo, this, &AbstractStream::updateLastMsgsTo);
|
||||
QObject::connect(&settings, &Settings::changed, this, &AbstractStream::updateMasks);
|
||||
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &AbstractStream::updateMasks);
|
||||
QObject::connect(dbc(), &DBCManager::maskUpdated, this, &AbstractStream::updateMasks);
|
||||
QObject::connect(this, &AbstractStream::streamStarted, [this]() {
|
||||
emit StreamNotifier::instance()->changingStream();
|
||||
delete can;
|
||||
can = this;
|
||||
// TODO: add method stop() to class AbstractStream
|
||||
QObject::connect(qApp, &QApplication::aboutToQuit, can, []() {
|
||||
qDebug() << "stopping stream thread";
|
||||
can->pause(true);
|
||||
});
|
||||
emit StreamNotifier::instance()->streamStarted();
|
||||
});
|
||||
}
|
||||
|
||||
void AbstractStream::updateMasks() {
|
||||
std::lock_guard lk(mutex);
|
||||
masks.clear();
|
||||
if (settings.suppress_defined_signals) {
|
||||
for (auto s : sources) {
|
||||
if (auto f = dbc()->findDBCFile(s)) {
|
||||
for (const auto &[address, m] : f->getMessages()) {
|
||||
masks[{.source = (uint8_t)s, .address = address}] = m.mask;
|
||||
}
|
||||
std::lock_guard lk(mutex_);
|
||||
masks_.clear();
|
||||
if (!settings.suppress_defined_signals)
|
||||
return;
|
||||
|
||||
for (const auto s : sources) {
|
||||
for (const auto &[address, m] : dbc()->getMessages(s)) {
|
||||
masks_[{.source = (uint8_t)s, .address = address}] = m.mask;
|
||||
}
|
||||
}
|
||||
// clear bit change counts
|
||||
for (auto &[id, m] : messages_) {
|
||||
auto &mask = masks_[id];
|
||||
const int size = std::min(mask.size(), m.last_changes.size());
|
||||
for (int i = 0; i < size; ++i) {
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
if (((mask[i] >> (7 - j)) & 1) != 0) m.last_changes[i].bit_change_counts[j] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractStream::updateMessages(QHash<MessageId, CanData> *messages) {
|
||||
void AbstractStream::suppressDefinedSignals(bool suppress) {
|
||||
settings.suppress_defined_signals = suppress;
|
||||
updateMasks();
|
||||
}
|
||||
|
||||
size_t AbstractStream::suppressHighlighted() {
|
||||
std::lock_guard lk(mutex_);
|
||||
size_t cnt = 0;
|
||||
const double cur_ts = currentSec();
|
||||
for (auto &[_, m] : messages_) {
|
||||
for (auto &last_change : m.last_changes) {
|
||||
const double dt = cur_ts - last_change.ts;
|
||||
if (dt < 2.0) {
|
||||
last_change.suppressed = true;
|
||||
}
|
||||
// clear bit change counts
|
||||
last_change.bit_change_counts.fill(0);
|
||||
cnt += last_change.suppressed;
|
||||
}
|
||||
}
|
||||
return cnt;
|
||||
}
|
||||
|
||||
void AbstractStream::clearSuppressed() {
|
||||
std::lock_guard lk(mutex_);
|
||||
for (auto &[_, m] : messages_) {
|
||||
std::for_each(m.last_changes.begin(), m.last_changes.end(), [](auto &c) { c.suppressed = false; });
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractStream::updateLastMessages() {
|
||||
auto prev_src_size = sources.size();
|
||||
auto prev_msg_size = last_msgs.size();
|
||||
for (auto it = messages->begin(); it != messages->end(); ++it) {
|
||||
const auto &id = it.key();
|
||||
last_msgs[id] = it.value();
|
||||
sources.insert(id.source);
|
||||
std::set<MessageId> msgs;
|
||||
{
|
||||
std::lock_guard lk(mutex_);
|
||||
for (const auto &id : new_msgs_) {
|
||||
last_msgs[id] = messages_[id];
|
||||
sources.insert(id.source);
|
||||
}
|
||||
msgs = std::move(new_msgs_);
|
||||
}
|
||||
|
||||
if (sources.size() != prev_src_size) {
|
||||
updateMasks();
|
||||
emit sourcesUpdated(sources);
|
||||
}
|
||||
emit updated();
|
||||
emit msgsReceived(messages, prev_msg_size != last_msgs.size());
|
||||
delete messages;
|
||||
processing = false;
|
||||
emit msgsReceived(&msgs, prev_msg_size != last_msgs.size());
|
||||
}
|
||||
|
||||
void AbstractStream::updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size) {
|
||||
std::lock_guard lk(mutex);
|
||||
auto mask_it = masks.find(id);
|
||||
std::vector<uint8_t> *mask = mask_it == masks.end() ? nullptr : &mask_it->second;
|
||||
all_msgs[id].compute(id, (const char *)data, size, sec, getSpeed(), mask);
|
||||
if (!new_msgs->contains(id)) {
|
||||
new_msgs->insert(id, {});
|
||||
}
|
||||
}
|
||||
|
||||
bool AbstractStream::postEvents() {
|
||||
// delay posting CAN message if UI thread is busy
|
||||
if (processing == false) {
|
||||
processing = true;
|
||||
for (auto it = new_msgs->begin(); it != new_msgs->end(); ++it) {
|
||||
it.value() = all_msgs[it.key()];
|
||||
}
|
||||
// use pointer to avoid data copy in queued connection.
|
||||
QMetaObject::invokeMethod(this, std::bind(&AbstractStream::updateMessages, this, new_msgs.release()), Qt::QueuedConnection);
|
||||
new_msgs.reset(new QHash<MessageId, CanData>);
|
||||
new_msgs->reserve(100);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
std::lock_guard lk(mutex_);
|
||||
messages_[id].compute(id, data, size, sec, getSpeed(), masks_[id]);
|
||||
new_msgs_.insert(id);
|
||||
}
|
||||
|
||||
const std::vector<const CanEvent *> &AbstractStream::events(const MessageId &id) const {
|
||||
@@ -102,76 +119,62 @@ const std::vector<const CanEvent *> &AbstractStream::events(const MessageId &id)
|
||||
const CanData &AbstractStream::lastMessage(const MessageId &id) {
|
||||
static CanData empty_data = {};
|
||||
auto it = last_msgs.find(id);
|
||||
return it != last_msgs.end() ? it.value() : empty_data;
|
||||
return it != last_msgs.end() ? it->second : empty_data;
|
||||
}
|
||||
|
||||
// it is thread safe to update data in updateLastMsgsTo.
|
||||
// updateLastMsgsTo is always called in UI thread.
|
||||
void AbstractStream::updateLastMsgsTo(double sec) {
|
||||
new_msgs.reset(new QHash<MessageId, CanData>);
|
||||
all_msgs.clear();
|
||||
last_msgs.clear();
|
||||
new_msgs_.clear();
|
||||
messages_.clear();
|
||||
|
||||
uint64_t last_ts = (sec + routeStartTime()) * 1e9;
|
||||
for (auto &[id, ev] : events_) {
|
||||
auto it = std::lower_bound(ev.crbegin(), ev.crend(), last_ts, [](auto e, uint64_t ts) {
|
||||
return e->mono_time > ts;
|
||||
});
|
||||
auto mask_it = masks.find(id);
|
||||
std::vector<uint8_t> *mask = mask_it == masks.end() ? nullptr : &mask_it->second;
|
||||
if (it != ev.crend()) {
|
||||
double ts = (*it)->mono_time / 1e9 - routeStartTime();
|
||||
auto &m = all_msgs[id];
|
||||
m.compute(id, (const char *)(*it)->dat, (*it)->size, ts, getSpeed(), mask);
|
||||
m.count = std::distance(it, ev.crend());
|
||||
for (const auto &[id, ev] : events_) {
|
||||
auto it = std::upper_bound(ev.begin(), ev.end(), last_ts, CompareCanEvent());
|
||||
if (it != ev.begin()) {
|
||||
auto prev = std::prev(it);
|
||||
double ts = (*prev)->mono_time / 1e9 - routeStartTime();
|
||||
auto &m = messages_[id];
|
||||
m.compute(id, (*prev)->dat, (*prev)->size, ts, getSpeed(), {});
|
||||
m.count = std::distance(ev.begin(), prev) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// deep copy all_msgs to last_msgs to avoid multi-threading issue.
|
||||
last_msgs = all_msgs;
|
||||
last_msgs.detach();
|
||||
// use a timer to prevent recursive calls
|
||||
QTimer::singleShot(0, [this]() {
|
||||
emit updated();
|
||||
emit msgsReceived(&last_msgs, true);
|
||||
});
|
||||
bool id_changed = messages_.size() != last_msgs.size() ||
|
||||
std::any_of(messages_.cbegin(), messages_.cend(),
|
||||
[this](const auto &m) { return !last_msgs.count(m.first); });
|
||||
last_msgs = messages_;
|
||||
emit msgsReceived(nullptr, id_changed);
|
||||
}
|
||||
|
||||
void AbstractStream::mergeEvents(std::vector<Event *>::const_iterator first, std::vector<Event *>::const_iterator last) {
|
||||
const CanEvent *AbstractStream::newEvent(uint64_t mono_time, const cereal::CanData::Reader &c) {
|
||||
auto dat = c.getDat();
|
||||
CanEvent *e = (CanEvent *)event_buffer_->allocate(sizeof(CanEvent) + sizeof(uint8_t) * dat.size());
|
||||
e->src = c.getSrc();
|
||||
e->address = c.getAddress();
|
||||
e->mono_time = mono_time;
|
||||
e->size = dat.size();
|
||||
memcpy(e->dat, (uint8_t *)dat.begin(), e->size);
|
||||
return e;
|
||||
}
|
||||
|
||||
void AbstractStream::mergeEvents(const std::vector<const CanEvent *> &events) {
|
||||
static MessageEventsMap msg_events;
|
||||
static std::vector<const CanEvent *> new_events;
|
||||
|
||||
std::for_each(msg_events.begin(), msg_events.end(), [](auto &e) { e.second.clear(); });
|
||||
new_events.clear();
|
||||
|
||||
for (auto it = first; it != last; ++it) {
|
||||
if ((*it)->which == cereal::Event::Which::CAN) {
|
||||
uint64_t ts = (*it)->mono_time;
|
||||
for (const auto &c : (*it)->event.getCan()) {
|
||||
auto dat = c.getDat();
|
||||
CanEvent *e = (CanEvent *)event_buffer->allocate(sizeof(CanEvent) + sizeof(uint8_t) * dat.size());
|
||||
e->src = c.getSrc();
|
||||
e->address = c.getAddress();
|
||||
e->mono_time = ts;
|
||||
e->size = dat.size();
|
||||
memcpy(e->dat, (uint8_t *)dat.begin(), e->size);
|
||||
|
||||
msg_events[{.source = e->src, .address = e->address}].push_back(e);
|
||||
new_events.push_back(e);
|
||||
}
|
||||
}
|
||||
for (auto e : events) {
|
||||
msg_events[{.source = e->src, .address = e->address}].push_back(e);
|
||||
}
|
||||
|
||||
if (!new_events.empty()) {
|
||||
for (auto &[id, new_e] : msg_events) {
|
||||
if (!events.empty()) {
|
||||
for (const auto &[id, new_e] : msg_events) {
|
||||
if (!new_e.empty()) {
|
||||
auto &e = events_[id];
|
||||
auto pos = std::upper_bound(e.cbegin(), e.cend(), new_e.front()->mono_time, CompareCanEvent());
|
||||
e.insert(pos, new_e.cbegin(), new_e.cend());
|
||||
}
|
||||
}
|
||||
auto pos = std::upper_bound(all_events_.cbegin(), all_events_.cend(), new_events.front()->mono_time, CompareCanEvent());
|
||||
all_events_.insert(pos, new_events.cbegin(), new_events.cend());
|
||||
auto pos = std::upper_bound(all_events_.cbegin(), all_events_.cend(), events.front()->mono_time, CompareCanEvent());
|
||||
all_events_.insert(pos, events.cbegin(), events.cend());
|
||||
emit eventsMerged(msg_events);
|
||||
}
|
||||
lastest_event_ts = all_events_.empty() ? 0 : all_events_.back()->mono_time;
|
||||
@@ -181,15 +184,16 @@ void AbstractStream::mergeEvents(std::vector<Event *>::const_iterator first, std
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int periodic_threshold = 10;
|
||||
constexpr int start_alpha = 128;
|
||||
constexpr float fade_time = 2.0;
|
||||
const QColor CYAN = QColor(0, 187, 255, start_alpha);
|
||||
const QColor RED = QColor(255, 0, 0, start_alpha);
|
||||
const QColor GREYISH_BLUE = QColor(102, 86, 169, start_alpha / 2);
|
||||
const QColor CYAN_LIGHTER = QColor(0, 187, 255, start_alpha).lighter(135);
|
||||
const QColor RED_LIGHTER = QColor(255, 0, 0, start_alpha).lighter(135);
|
||||
const QColor GREYISH_BLUE_LIGHTER = QColor(102, 86, 169, start_alpha / 2).lighter(135);
|
||||
enum Color { GREYISH_BLUE, CYAN, RED};
|
||||
QColor getColor(int c) {
|
||||
constexpr int start_alpha = 128;
|
||||
static const QColor colors[] = {
|
||||
[GREYISH_BLUE] = QColor(102, 86, 169, start_alpha / 2),
|
||||
[CYAN] = QColor(0, 187, 255, start_alpha),
|
||||
[RED] = QColor(255, 0, 0, start_alpha),
|
||||
};
|
||||
return settings.theme == LIGHT_THEME ? colors[c] : colors[c].lighter(135);
|
||||
}
|
||||
|
||||
inline QColor blend(const QColor &a, const QColor &b) {
|
||||
return QColor((a.red() + b.red()) / 2, (a.green() + b.green()) / 2, (a.blue() + b.blue()) / 2, (a.alpha() + b.alpha()) / 2);
|
||||
@@ -212,8 +216,8 @@ double calc_freq(const MessageId &msg_id, double current_sec) {
|
||||
|
||||
} // namespace
|
||||
|
||||
void CanData::compute(const MessageId &msg_id, const char *can_data, const int size, double current_sec,
|
||||
double playback_speed, const std::vector<uint8_t> *mask, double in_freq) {
|
||||
void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const int size, double current_sec,
|
||||
double playback_speed, const std::vector<uint8_t> &mask, double in_freq) {
|
||||
ts = current_sec;
|
||||
++count;
|
||||
|
||||
@@ -224,55 +228,53 @@ void CanData::compute(const MessageId &msg_id, const char *can_data, const int s
|
||||
|
||||
if (dat.size() != size) {
|
||||
dat.resize(size);
|
||||
bit_change_counts.resize(size);
|
||||
colors = QVector(size, QColor(0, 0, 0, 0));
|
||||
last_change_t.assign(size, ts);
|
||||
last_delta.resize(size);
|
||||
same_delta_counter.resize(size);
|
||||
colors.assign(size, QColor(0, 0, 0, 0));
|
||||
last_changes.resize(size);
|
||||
std::for_each(last_changes.begin(), last_changes.end(), [current_sec](auto &c) { c.ts = current_sec; });
|
||||
} else {
|
||||
bool lighter = settings.theme == DARK_THEME;
|
||||
const QColor &cyan = !lighter ? CYAN : CYAN_LIGHTER;
|
||||
const QColor &red = !lighter ? RED : RED_LIGHTER;
|
||||
const QColor &greyish_blue = !lighter ? GREYISH_BLUE : GREYISH_BLUE_LIGHTER;
|
||||
constexpr int periodic_threshold = 10;
|
||||
constexpr float fade_time = 2.0;
|
||||
const float alpha_delta = 1.0 / (freq + 1) / (fade_time * playback_speed);
|
||||
|
||||
for (int i = 0; i < size; ++i) {
|
||||
const uint8_t mask_byte = (mask && i < mask->size()) ? (~((*mask)[i])) : 0xff;
|
||||
auto &last_change = last_changes[i];
|
||||
|
||||
uint8_t mask_byte = last_change.suppressed ? 0x00 : 0xFF;
|
||||
if (i < mask.size()) mask_byte &= ~(mask[i]);
|
||||
|
||||
const uint8_t last = dat[i] & mask_byte;
|
||||
const uint8_t cur = can_data[i] & mask_byte;
|
||||
const int delta = cur - last;
|
||||
|
||||
if (last != cur) {
|
||||
double delta_t = ts - last_change_t[i];
|
||||
|
||||
const int delta = cur - last;
|
||||
// Keep track if signal is changing randomly, or mostly moving in the same direction
|
||||
if (std::signbit(delta) == std::signbit(last_delta[i])) {
|
||||
same_delta_counter[i] = std::min(16, same_delta_counter[i] + 1);
|
||||
if (std::signbit(delta) == std::signbit(last_change.delta)) {
|
||||
last_change.same_delta_counter = std::min(16, last_change.same_delta_counter + 1);
|
||||
} else {
|
||||
same_delta_counter[i] = std::max(0, same_delta_counter[i] - 4);
|
||||
last_change.same_delta_counter = std::max(0, last_change.same_delta_counter - 4);
|
||||
}
|
||||
|
||||
const double delta_t = ts - last_change.ts;
|
||||
// Mostly moves in the same direction, color based on delta up/down
|
||||
if (delta_t * freq > periodic_threshold || same_delta_counter[i] > 8) {
|
||||
if (delta_t * freq > periodic_threshold || last_change.same_delta_counter > 8) {
|
||||
// Last change was while ago, choose color based on delta up or down
|
||||
colors[i] = (cur > last) ? cyan : red;
|
||||
colors[i] = getColor(cur > last ? CYAN : RED);
|
||||
} else {
|
||||
// Periodic changes
|
||||
colors[i] = blend(colors[i], greyish_blue);
|
||||
colors[i] = blend(colors[i], getColor(GREYISH_BLUE));
|
||||
}
|
||||
|
||||
// Track bit level changes
|
||||
const uint8_t tmp = (cur ^ last);
|
||||
for (int bit = 0; bit < 8; bit++) {
|
||||
if (tmp & (1 << bit)) {
|
||||
bit_change_counts[i][bit] += 1;
|
||||
if (tmp & (1 << (7 - bit))) {
|
||||
last_change.bit_change_counts[bit] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
last_change_t[i] = ts;
|
||||
last_delta[i] = delta;
|
||||
last_change.ts = ts;
|
||||
last_change.delta = delta;
|
||||
} else {
|
||||
// Fade out
|
||||
float alpha_delta = 1.0 / (freq + 1) / (fade_time * playback_speed);
|
||||
colors[i].setAlphaF(std::max(0.0, colors[i].alphaF() - alpha_delta));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <QColor>
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
|
||||
#include "common/timing.h"
|
||||
#include "cereal/messaging/messaging.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/util.h"
|
||||
#include "tools/replay/replay.h"
|
||||
|
||||
struct CanData {
|
||||
void compute(const MessageId &msg_id, const char *dat, const int size, double current_sec,
|
||||
double playback_speed, const std::vector<uint8_t> *mask = nullptr, double in_freq = 0);
|
||||
void compute(const MessageId &msg_id, const uint8_t *dat, const int size, double current_sec,
|
||||
double playback_speed, const std::vector<uint8_t> &mask, double in_freq = 0);
|
||||
|
||||
double ts = 0.;
|
||||
uint32_t count = 0;
|
||||
double freq = 0;
|
||||
QByteArray dat;
|
||||
QVector<QColor> colors;
|
||||
std::vector<double> last_change_t;
|
||||
std::vector<std::array<uint32_t, 8>> bit_change_counts;
|
||||
std::vector<int> last_delta;
|
||||
std::vector<int> same_delta_counter;
|
||||
std::vector<uint8_t> dat;
|
||||
std::vector<QColor> colors;
|
||||
|
||||
struct ByteLastChange {
|
||||
double ts;
|
||||
int delta;
|
||||
int same_delta_counter;
|
||||
bool suppressed;
|
||||
std::array<uint32_t, 8> bit_change_counts;
|
||||
};
|
||||
std::vector<ByteLastChange> last_changes;
|
||||
double last_freq_update_ts = 0;
|
||||
};
|
||||
|
||||
@@ -61,7 +63,7 @@ public:
|
||||
AbstractStream(QObject *parent);
|
||||
virtual ~AbstractStream() {}
|
||||
virtual void start() = 0;
|
||||
inline bool liveStreaming() const { return route() == nullptr; }
|
||||
virtual bool liveStreaming() const { return true; }
|
||||
virtual void seekTo(double ts) {}
|
||||
virtual QString routeName() const = 0;
|
||||
virtual QString carFingerprint() const { return ""; }
|
||||
@@ -69,17 +71,20 @@ public:
|
||||
virtual double routeStartTime() const { return 0; }
|
||||
virtual double currentSec() const = 0;
|
||||
virtual double totalSeconds() const { return lastEventMonoTime() / 1e9 - routeStartTime(); }
|
||||
const CanData &lastMessage(const MessageId &id);
|
||||
virtual VisionStreamType visionStreamType() const { return VISION_STREAM_ROAD; }
|
||||
virtual const Route *route() const { return nullptr; }
|
||||
virtual void setSpeed(float speed) {}
|
||||
virtual double getSpeed() { return 1; }
|
||||
virtual bool isPaused() const { return false; }
|
||||
virtual void pause(bool pause) {}
|
||||
const MessageEventsMap &eventsMap() const { return events_; }
|
||||
const std::vector<const CanEvent *> &allEvents() const { return all_events_; }
|
||||
|
||||
inline const std::unordered_map<MessageId, CanData> &lastMessages() const { return last_msgs; }
|
||||
inline const MessageEventsMap &eventsMap() const { return events_; }
|
||||
inline const std::vector<const CanEvent *> &allEvents() const { return all_events_; }
|
||||
const CanData &lastMessage(const MessageId &id);
|
||||
const std::vector<const CanEvent *> &events(const MessageId &id) const;
|
||||
virtual const std::vector<std::tuple<double, double, TimelineType>> getTimeline() { return {}; }
|
||||
|
||||
size_t suppressHighlighted();
|
||||
void clearSuppressed();
|
||||
void suppressDefinedSignals(bool suppress);
|
||||
|
||||
signals:
|
||||
void paused();
|
||||
@@ -87,32 +92,36 @@ signals:
|
||||
void seekedTo(double sec);
|
||||
void streamStarted();
|
||||
void eventsMerged(const MessageEventsMap &events_map);
|
||||
void updated();
|
||||
void msgsReceived(const QHash<MessageId, CanData> *new_msgs, bool has_new_ids);
|
||||
void msgsReceived(const std::set<MessageId> *new_msgs, bool has_new_ids);
|
||||
void sourcesUpdated(const SourceSet &s);
|
||||
void privateUpdateLastMsgsSignal();
|
||||
|
||||
public:
|
||||
QHash<MessageId, CanData> last_msgs;
|
||||
SourceSet sources;
|
||||
|
||||
protected:
|
||||
void mergeEvents(std::vector<Event *>::const_iterator first, std::vector<Event *>::const_iterator last);
|
||||
bool postEvents();
|
||||
uint64_t lastEventMonoTime() const { return lastest_event_ts; }
|
||||
void mergeEvents(const std::vector<const CanEvent *> &events);
|
||||
const CanEvent *newEvent(uint64_t mono_time, const cereal::CanData::Reader &c);
|
||||
void updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size);
|
||||
void updateMessages(QHash<MessageId, CanData> *);
|
||||
void updateMasks();
|
||||
void updateLastMsgsTo(double sec);
|
||||
uint64_t lastEventMonoTime() const { return lastest_event_ts; }
|
||||
|
||||
uint64_t lastest_event_ts = 0;
|
||||
std::atomic<bool> processing = false;
|
||||
std::unique_ptr<QHash<MessageId, CanData>> new_msgs;
|
||||
QHash<MessageId, CanData> all_msgs;
|
||||
MessageEventsMap events_;
|
||||
std::vector<const CanEvent *> all_events_;
|
||||
std::unique_ptr<MonotonicBuffer> event_buffer;
|
||||
std::mutex mutex;
|
||||
std::unordered_map<MessageId, std::vector<uint8_t>> masks;
|
||||
uint64_t lastest_event_ts = 0;
|
||||
|
||||
private:
|
||||
void updateLastMessages();
|
||||
void updateLastMsgsTo(double sec);
|
||||
void updateMasks();
|
||||
|
||||
MessageEventsMap events_;
|
||||
std::unordered_map<MessageId, CanData> last_msgs;
|
||||
std::unique_ptr<MonotonicBuffer> event_buffer_;
|
||||
|
||||
// Members accessed in multiple threads. (mutex protected)
|
||||
std::mutex mutex_;
|
||||
std::set<MessageId> new_msgs_;
|
||||
std::unordered_map<MessageId, CanData> messages_;
|
||||
std::unordered_map<MessageId, std::vector<uint8_t>> masks_;
|
||||
};
|
||||
|
||||
class AbstractOpenStreamWidget : public QWidget {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user