diff --git a/.github/workflows/repo.yml b/.github/workflows/repo.yml new file mode 100644 index 0000000000..1445aa635d --- /dev/null +++ b/.github/workflows/repo.yml @@ -0,0 +1,28 @@ +name: repo + +on: + schedule: + - cron: "0 15 * * 2" + workflow_dispatch: + +jobs: + pre-commit-autoupdate: + name: pre-commit autoupdate + runs-on: ubuntu-20.04 + container: + image: ghcr.io/commaai/openpilot-base:latest + steps: + - uses: actions/checkout@v3 + - name: pre-commit autoupdate + run: | + git config --global --add safe.directory '*' + pre-commit autoupdate + - name: Create Pull Request + uses: peter-evans/create-pull-request@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5 + with: + token: ${{ secrets.ACTIONS_CREATE_PR_PAT }} + commit-message: Update pre-commit hook versions + title: 'pre-commit: autoupdate hooks' + branch: pre-commit-updates + base: master + delete-branch: true diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 5873e7aee6..0f782107ec 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -261,7 +261,7 @@ jobs: name: process_replay_diff.txt path: selfdrive/test/process_replay/diff.txt - name: Upload reference logs - if: ${{ failure() && steps.print-diff.outcome == 'success' && github.event_name == 'pull_request' && github.repository == 'commaai/openpilot' && env.AZURE_TOKEN != '' }} + if: ${{ failure() && steps.print-diff.outcome == 'success' && github.repository == 'commaai/openpilot' && env.AZURE_TOKEN != '' }} run: | ${{ env.RUN }} "CI=1 AZURE_TOKEN='$AZURE_TOKEN' python selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only" - name: "Upload coverage to Codecov" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1779947ff9..0b021adfe4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,11 +4,12 @@ repos: - id: check-hooks-apply - id: check-useless-excludes - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v4.4.0 hooks: - id: check-ast exclude: '^(third_party)/' - id: check-json + - id: check-toml - id: check-xml - id: check-yaml - id: check-merge-conflict @@ -16,7 +17,7 @@ repos: - id: check-added-large-files args: ['--maxkb=100'] - repo: https://github.com/codespell-project/codespell - rev: v2.2.1 + rev: v2.2.4 hooks: - id: codespell exclude: '^(third_party/)|(body/)|(cereal/)|(rednose/)|(panda/)|(laika/)|(opendbc/)|(laika_repo/)|(rednose_repo/)|(selfdrive/ui/translations/.*.ts)|(poetry.lock)' @@ -33,7 +34,7 @@ repos: types: [python] exclude: '^(third_party/)|(cereal/)|(opendbc/)|(panda/)|(laika/)|(laika_repo/)|(rednose/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(xx/)' - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 6.0.0 hooks: - id: flake8 exclude: '^(third_party/)|(cereal/)|(rednose/)|(panda/)|(laika/)|(opendbc/)|(laika_repo/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(selfdrive/debug/)/' @@ -79,6 +80,6 @@ repos: language: script pass_filenames: false - repo: https://github.com/python-poetry/poetry - rev: '1.2.2' + rev: '1.4.0' hooks: - id: poetry-check diff --git a/Jenkinsfile b/Jenkinsfile index 4e80b5fa3c..88f98f33e9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -125,6 +125,21 @@ pipeline { } */ + stage('tizi-tests') { + agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } } + steps { + phone_steps("tizi", [ + ["build openpilot", "cd selfdrive/manager && ./build.py"], + ["test boardd loopback", "SINGLE_PANDA=1 python selfdrive/boardd/tests/test_boardd_loopback.py"], + ["test pandad", "python selfdrive/boardd/tests/test_pandad.py"], + ["test sensord", "cd system/sensord/tests && python -m unittest test_sensord.py"], + ["test camerad", "python system/camerad/test/test_camerad.py"], + ["test exposure", "python system/camerad/test/test_exposure.py"], + ["test amp", "python system/hardware/tici/tests/test_amplifier.py"], + ]) + } + } + stage('build') { agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } } environment { @@ -156,7 +171,7 @@ pipeline { steps { phone_steps("tici-common", [ ["build", "cd selfdrive/manager && ./build.py"], - ["test power draw", "python system/hardware/tici/test_power_draw.py"], + ["test power draw", "python system/hardware/tici/tests/test_power_draw.py"], ["test loggerd", "python system/loggerd/tests/test_loggerd.py"], ["test encoder", "LD_LIBRARY_PATH=/usr/local/lib python system/loggerd/tests/test_encoder.py"], ["test pigeond", "python system/sensord/tests/test_pigeond.py"], diff --git a/RELEASES.md b/RELEASES.md index 55b0dbad7e..4f5708cc1a 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,7 +1,12 @@ Version 0.9.2 (2023-03-XX) ======================== +* New driving model, trained on a new dataset * Draw MPC path instead of model predicted path, this is a more accurate representation of what the car will do. * Buick LaCrosse 2017-19 support thanks to koch-cf! +* Chevrolet Trailblazer 2021-22 support thanks to TurboCE! +* Honda HR-V 2023 support thanks to AlexandreSato and galegozi! +* Kia Niro EV 2023 support thanks to JosselinLecocq! +* Lexus ES 2017-18 support * Škoda Fabia 2022-23 support thanks to jyoung8607! Version 0.9.1 (2023-02-28) diff --git a/SConstruct b/SConstruct index 387d2e76f4..320590b058 100644 --- a/SConstruct +++ b/SConstruct @@ -5,6 +5,10 @@ import sysconfig import platform import numpy as np +import SCons.Errors + +SCons.Warnings.warningAsException(True) + TICI = os.path.isfile('/TICI') AGNOS = TICI @@ -118,7 +122,7 @@ else: f"#third_party/libyuv/{yuv_dir}/lib", f"{brew_prefix}/lib", f"{brew_prefix}/Library", - f"{brew_prefix}/opt/openssl/lib", + f"{brew_prefix}/opt/openssl@3.0/lib", f"{brew_prefix}/Cellar", "/System/Library/Frameworks/OpenGL.framework/Libraries", ] @@ -131,7 +135,7 @@ else: cxxflags += ["-DGL_SILENCE_DEPRECATION"] cpppath += [ f"{brew_prefix}/include", - f"{brew_prefix}/opt/openssl/include", + f"{brew_prefix}/opt/openssl@3.0/include", ] # Linux 86_64 else: @@ -310,8 +314,14 @@ else: qt_env.PrependENVPath('PATH', Dir("#third_party/qt5/larch64/bin/").abspath) elif arch != "Darwin": qt_libs += ["GL"] +qt_env['QT3DIR'] = qt_env['QTDIR'] + +# compatibility for older SCons versions +try: + qt_env.Tool('qt3') +except SCons.Errors.UserError: + qt_env.Tool('qt') -qt_env.Tool('qt') qt_env['CPPPATH'] += qt_dirs + ["#selfdrive/ui/qt/"] qt_flags = [ "-D_REENTRANT", @@ -432,11 +442,8 @@ SConscript(['system/sensord/SConscript']) SConscript(['selfdrive/ui/SConscript']) SConscript(['selfdrive/navd/SConscript']) -if arch in ['x86_64', 'Darwin'] or GetOption('extras'): +if (arch in ['x86_64', 'Darwin'] and Dir('#tools/cabana/').exists()) or GetOption('extras'): SConscript(['tools/replay/SConscript']) - - opendbc = abspath([File('opendbc/can/libdbc.so')]) - Export('opendbc') SConscript(['tools/cabana/SConscript']) external_sconscript = GetOption('external_sconscript') diff --git a/cereal b/cereal index 7492dc3f45..37157b1364 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit 7492dc3f45702dfdbe0f353f38844e7fbf39ca71 +Subproject commit 37157b136403ad42bb262cae2abcb5329392d1f4 diff --git a/common/gpio.py b/common/gpio.py index 260f8898a1..711fcff85d 100644 --- a/common/gpio.py +++ b/common/gpio.py @@ -1,4 +1,5 @@ -from typing import Optional +import glob +from typing import Optional, List def gpio_init(pin: int, output: bool) -> None: try: @@ -23,3 +24,13 @@ def gpio_read(pin: int) -> Optional[bool]: print(f"Failed to set gpio {pin} value: {e}") return val + +def get_irq_for_action(action: str) -> List[int]: + ret = [] + for fn in glob.glob('/sys/kernel/irq/*/actions'): + with open(fn) as f: + actions = f.read().strip().split(',') + if action in actions: + irq = int(fn.split('/')[-2]) + ret.append(irq) + return ret diff --git a/common/i2c.cc b/common/i2c.cc index eb10cd64bb..ef788ac9ea 100644 --- a/common/i2c.cc +++ b/common/i2c.cc @@ -15,7 +15,7 @@ #define UNUSED(x) (void)(x) #ifdef QCOM2 -// TODO: decide if we want to isntall libi2c-dev everywhere +// TODO: decide if we want to install libi2c-dev everywhere extern "C" { #include #include diff --git a/common/params.cc b/common/params.cc index e71f802d61..428830a112 100644 --- a/common/params.cc +++ b/common/params.cc @@ -98,6 +98,7 @@ std::unordered_map keys = { {"CarVin", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON}, {"CompletedTrainingVersion", PERSISTENT}, {"ControlsReady", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON}, + {"CurrentBootlog", PERSISTENT}, {"CurrentRoute", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON}, {"DisableLogging", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON}, {"DisablePowerDown", PERSISTENT}, @@ -110,7 +111,7 @@ std::unordered_map keys = { {"DoReboot", CLEAR_ON_MANAGER_START}, {"DoShutdown", CLEAR_ON_MANAGER_START}, {"DoUninstall", CLEAR_ON_MANAGER_START}, - {"FirmwareObdQueryDone", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON}, + {"FirmwareQueryDone", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON}, {"ForcePowerDown", CLEAR_ON_MANAGER_START}, {"GitBranch", PERSISTENT}, {"GitCommit", PERSISTENT}, @@ -154,7 +155,8 @@ std::unordered_map keys = { {"NavSettingTime24h", PERSISTENT}, {"NavSettingLeftSide", PERSISTENT}, {"NavdRender", PERSISTENT}, - {"ObdMultiplexingDisabled", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON}, + {"ObdMultiplexingChanged", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON}, + {"ObdMultiplexingEnabled", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON}, {"OpenpilotEnabledToggle", PERSISTENT}, {"PandaHeartbeatLost", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_OFF}, {"PandaSignatures", CLEAR_ON_MANAGER_START}, diff --git a/common/statlog.cc b/common/statlog.cc index 26945882d9..587f3e8620 100644 --- a/common/statlog.cc +++ b/common/statlog.cc @@ -5,6 +5,7 @@ #include "common/statlog.h" #include "common/util.h" +#include #include #include #include diff --git a/common/swaglog.cc b/common/swaglog.cc index 22682dc54c..54f1c3478a 100644 --- a/common/swaglog.cc +++ b/common/swaglog.cc @@ -5,6 +5,7 @@ #include "common/swaglog.h" #include +#include #include #include #include diff --git a/docs/CARS.md b/docs/CARS.md index 9a64e6d05c..97375055ad 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -2,9 +2,9 @@ # Supported Cars -A supported vehicle is one that just works when you install a comma three. All supported cars provide a better experience than any stock system. +A supported vehicle is one that just works when you install a comma three. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified. -# 239 Supported Cars +# 243 Supported Cars |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Harness|Video| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:| @@ -23,6 +23,7 @@ A supported vehicle is one that just works when you install a comma three. All s |Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|GM|| |Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|GM|| |Chevrolet|Silverado 1500 2020-21|Safety Package II|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|GM|| +|Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|GM|| |Chevrolet|Volt 2017-18[3](#footnotes)|Adaptive Cruise Control (ACC)|openpilot|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|OBD-II|| |Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|FCA|| |Chrysler|Pacifica 2019-20|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|FCA|| @@ -44,9 +45,9 @@ A supported vehicle is one that just works when you install a comma three. All s |Honda|Accord Hybrid 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Bosch A|| |Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Nidec|| |Honda|Civic 2019-21|All|openpilot available[1](#footnotes)|0 mph|2 mph[4](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Bosch A|| -|Honda|Civic 2022|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Bosch B|| +|Honda|Civic 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Bosch B|| |Honda|Civic Hatchback 2017-21|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Bosch A|| -|Honda|Civic Hatchback 2022|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Bosch B|| +|Honda|Civic Hatchback 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Bosch B|| |Honda|CR-V 2015-16|Touring Trim|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Nidec|| |Honda|CR-V 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Bosch A|| |Honda|CR-V Hybrid 2017-19|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Bosch A|| @@ -54,12 +55,13 @@ A supported vehicle is one that just works when you install a comma three. All s |Honda|Fit 2018-20|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Nidec|| |Honda|Freed 2020|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Nidec|| |Honda|HR-V 2019-22|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Nidec|| +|Honda|HR-V 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Bosch B|| |Honda|Insight 2019-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Bosch A|| |Honda|Inspire 2018|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Bosch A|| |Honda|Odyssey 2018-20|Honda Sensing|openpilot|25 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Nidec|| |Honda|Passport 2019-21|All|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Nidec|| |Honda|Pilot 2016-22|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Nidec|| -|Honda|Ridgeline 2017-22|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Nidec|| +|Honda|Ridgeline 2017-23|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Nidec|| |Hyundai|Elantra 2017-19|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai B|| |Hyundai|Elantra 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai K|| |Hyundai|Elantra GT 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai E|| @@ -74,7 +76,7 @@ A supported vehicle is one that just works when you install a comma three. All s |Hyundai|Ioniq Hybrid 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai C|| |Hyundai|Ioniq Hybrid 2020-22|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai H|| |Hyundai|Ioniq Plug-in Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai C|| -|Hyundai|Ioniq Plug-in Hybrid 2020-21|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai H|| +|Hyundai|Ioniq Plug-in Hybrid 2020-22|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai H|| |Hyundai|Kona 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai B|| |Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai G|| |Hyundai|Kona Electric 2022|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai O|| @@ -98,8 +100,8 @@ A supported vehicle is one that just works when you install a comma three. All s |Jeep|Grand Cherokee 2019-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|FCA|| |Kia|Ceed 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai E|| |Kia|EV6 (Southeast Asia only) 2022-23[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai P|| -|Kia|EV6 (with HDA II) 2022[5](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai P|| -|Kia|EV6 (without HDA II) 2022[5](#footnotes)|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai L|| +|Kia|EV6 (with HDA II) 2022-23[5](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai P|| +|Kia|EV6 (without HDA II) 2022-23[5](#footnotes)|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai L|| |Kia|Forte 2019-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai G|| |Kia|K5 2021-22|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai A|| |Kia|K5 Hybrid 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai A|| @@ -107,8 +109,8 @@ A supported vehicle is one that just works when you install a comma three. All s |Kia|Niro EV 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai F|| |Kia|Niro EV 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai C|| |Kia|Niro EV 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai H|| -|Kia|Niro Hybrid 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai F|| -|Kia|Niro Hybrid 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai H|| +|Kia|Niro EV 2023[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai A|| +|Kia|Niro Hybrid 2021-22|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai F|| |Kia|Niro Hybrid 2023[5](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai A|| |Kia|Niro Plug-in Hybrid 2018-19|All|openpilot available[1](#footnotes)|10 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai C|| |Kia|Niro Plug-in Hybrid 2020|All|openpilot available[1](#footnotes)|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai D|| @@ -125,15 +127,16 @@ A supported vehicle is one that just works when you install a comma three. All s |Kia|Stinger 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai K|| |Kia|Telluride 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai H|| |Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota|| +|Lexus|ES 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota|| |Lexus|ES 2019-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota|| -|Lexus|ES Hybrid 2017-18|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota|| +|Lexus|ES Hybrid 2017-18|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota|| |Lexus|ES Hybrid 2019-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota|| |Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota|| |Lexus|NX 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota|| |Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota|| |Lexus|NX Hybrid 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota|| |Lexus|NX Hybrid 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota|| -|Lexus|RC 2017-20|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota|| +|Lexus|RC 2018-20|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota|| |Lexus|RX 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota|| |Lexus|RX 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota|| |Lexus|RX 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota|| @@ -146,10 +149,10 @@ A supported vehicle is one that just works when you install a comma three. All s |Mazda|CX-5 2022-23|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Mazda|| |Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Mazda|| |Nissan|Altima 2019-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Nissan B|| -|Nissan|Leaf 2018-22|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Nissan A|| +|Nissan|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Nissan A|| |Nissan|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Nissan A|| |Nissan|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Nissan A|| -|Ram|1500 2019-22|Adaptive Cruise Control (ACC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Ram|| +|Ram|1500 2019-23|Adaptive Cruise Control (ACC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Ram|| |SEAT|Ateca 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533|| |SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533|| |Subaru|Ascent 2019-21|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Subaru A|| @@ -237,8 +240,8 @@ A supported vehicle is one that just works when you install a comma three. All s |Volkswagen|Passat 2015-22[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533|| |Volkswagen|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533|| |Volkswagen|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533|| -|Volkswagen|Polo 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533[10](#footnotes)|| -|Volkswagen|Polo GTI 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533[10](#footnotes)|| +|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533[10](#footnotes)|| +|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533[10](#footnotes)|| |Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533[10](#footnotes)|| |Volkswagen|T-Roc 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533[10](#footnotes)|| |Volkswagen|Taos 2022|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533|| @@ -246,6 +249,7 @@ A supported vehicle is one that just works when you install a comma three. All s |Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533|| |Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533|| |Volkswagen|Tiguan 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533|| +|Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533|| |Volkswagen|Touran 2017|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533|| diff --git a/docs/docker/Dockerfile b/docs/docker/Dockerfile index a1cdbbeaf0..08b318a59a 100644 --- a/docs/docker/Dockerfile +++ b/docs/docker/Dockerfile @@ -11,6 +11,7 @@ WORKDIR ${OPENPILOT_PATH} COPY SConstruct ${OPENPILOT_PATH} +COPY ./body ${OPENPILOT_PATH}/body COPY ./third_party ${OPENPILOT_PATH}/third_party COPY ./site_scons ${OPENPILOT_PATH}/site_scons COPY ./laika ${OPENPILOT_PATH}/laika diff --git a/laika_repo b/laika_repo index 6fadabd860..e932f32ab9 160000 --- a/laika_repo +++ b/laika_repo @@ -1 +1 @@ -Subproject commit 6fadabd86043ee19e06c6ed59aa4e688c14fa8e4 +Subproject commit e932f32ab921ed09ba6e304990574693d8ca5199 diff --git a/opendbc b/opendbc index b7d4a6e271..5adb62bf04 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit b7d4a6e2718d9ec3cf436441696528bedb1d44cf +Subproject commit 5adb62bf0483a3b3975e513b4e72d4abd9826aa6 diff --git a/poetry.lock b/poetry.lock index 4abd6777ba..5d60353f80 100644 --- a/poetry.lock +++ b/poetry.lock @@ -55,7 +55,7 @@ frozenlist = ">=1.1.0" name = "alabaster" version = "0.7.12" description = "A configurable sidebar-enabled Sphinx theme" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -119,6 +119,7 @@ packaging = ">20.6" [package.source] type = "url" url = "https://github.com/commaai/apex/releases/download/pytorch2.0.0%2Bcu11.8/apex-0.1-cp38-cp38-linux_x86_64.whl" + [[package]] name = "appdirs" version = "1.4.4" @@ -373,7 +374,7 @@ azure-nspkg = ">=2.0.0" name = "babel" version = "2.10.3" description = "Internationalization utilities" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -459,6 +460,14 @@ python-versions = "*" docutils = ">=0.12" Sphinx = ">=4.0,<5.0.0 || >5.0.0,<6" +[[package]] +name = "brotli" +version = "1.0.9" +description = "Python bindings for the Brotli compression library" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "cachecontrol" version = "0.12.11" @@ -847,7 +856,7 @@ python-versions = "*" name = "docutils" version = "0.17.1" description = "Docutils -- Python Documentation Utilities" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -1193,6 +1202,20 @@ monitor = ["psutil (>=5.7.0)"] recommended = ["backports.socketpair", "cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "psutil (>=5.7.0)", "selectors2"] test = ["backports.socketpair", "cffi (>=1.12.2)", "contextvars (==2.4)", "coverage (>=5.0)", "coveralls (>=1.7.0)", "dnspython (>=1.16.0,<2.0)", "futures", "idna", "mock", "objgraph", "psutil (>=5.7.0)", "requests", "selectors2"] +[[package]] +name = "geventhttpclient" +version = "2.0.2" +description = "http client library for gevent" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +brotli = "*" +certifi = "*" +gevent = ">=0.13" +six = "*" + [[package]] name = "greenlet" version = "1.1.3.post0" @@ -1367,7 +1390,7 @@ tifffile = ["tifffile"] name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -2496,6 +2519,33 @@ category = "main" optional = false python-versions = ">=3.8" +[[package]] +name = "nvidia-cublas-cu12" +version = "12.1.0.26" +description = "CUBLAS native runtime libraries" +category = "dev" +optional = false +python-versions = ">=3" + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.1.55" +description = "CUDA Runtime native Libraries" +category = "dev" +optional = false +python-versions = ">=3" + +[[package]] +name = "nvidia-cudnn-cu12" +version = "8.9.0.131" +description = "cuDNN runtime libraries" +category = "dev" +optional = false +python-versions = ">=3" + +[package.dependencies] +nvidia-cublas-cu12 = "*" + [[package]] name = "nvidia-ml-py3" version = "7.352.0" @@ -2608,6 +2658,7 @@ numpy = [ [package.source] type = "url" url = "https://github.com/commaai/opencv-python-builder/releases/download/4.5.5.64%2Bcu118/opencv_python_headless-4.5.5.64-cp38-cp38-manylinux_2_31_x86_64.whl" + [[package]] name = "osmium" version = "3.4.1" @@ -2679,6 +2730,23 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "panflute" +version = "2.3.0" +description = "Pythonic Pandoc filters" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +click = ">=6,<9" +pyyaml = ">=3,<7" + +[package.extras] +dev = ["configparser", "coverage", "flake8", "pandocfilters", "pytest", "pytest-cov", "requests"] +extras = ["yamlloader (>=1,<2)"] +pypi = ["Pygments", "docutils", "twine", "wheel"] + [[package]] name = "parameterized" version = "0.8.1" @@ -3091,7 +3159,7 @@ python-versions = ">=3.6" name = "pygments" version = "2.13.0" description = "Pygments is a syntax highlighting package written in Python." -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -3359,6 +3427,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "python-rapidjson" +version = "1.10" +description = "Python wrapper around rapidjson" +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "python-socketio" version = "5.7.2" @@ -3394,7 +3470,7 @@ numpy = ["numpy (>=1.6.0)"] name = "pytz" version = "2022.5" description = "World timezone definitions, modern and historical" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -3682,6 +3758,27 @@ python-versions = ">=3.6" [package.dependencies] setuptools = "*" +[[package]] +name = "sconscontrib" +version = "1.0" +description = "" +category = "main" +optional = false +python-versions = ">=3.6, <4" +develop = false + +[package.dependencies] +docutils = "*" +panflute = "*" +SCons = ">=4" +sphinx = "*" + +[package.source] +type = "git" +url = "https://github.com/SCons/scons-contrib.git" +reference = "HEAD" +resolved_reference = "f3b0100d3a628e4d18f496815903660a99489bae" + [[package]] name = "secretstorage" version = "3.3.3" @@ -3860,7 +3957,7 @@ python-versions = ">=3.7" name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "dev" +category = "main" optional = false python-versions = "*" @@ -3898,7 +3995,7 @@ python-versions = ">=3.6" name = "sphinx" version = "5.3.0" description = "Python documentation generator" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -3957,7 +4054,7 @@ sphinx = ">=1.2" name = "sphinxcontrib-applehelp" version = "1.0.2" description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" -category = "dev" +category = "main" optional = false python-versions = ">=3.5" @@ -3969,7 +4066,7 @@ test = ["pytest"] name = "sphinxcontrib-devhelp" version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -category = "dev" +category = "main" optional = false python-versions = ">=3.5" @@ -3981,7 +4078,7 @@ test = ["pytest"] name = "sphinxcontrib-htmlhelp" version = "2.0.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -3993,7 +4090,7 @@ test = ["html5lib", "pytest"] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" -category = "dev" +category = "main" optional = false python-versions = ">=3.5" @@ -4004,7 +4101,7 @@ test = ["flake8", "mypy", "pytest"] name = "sphinxcontrib-qthelp" version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -category = "dev" +category = "main" optional = false python-versions = ">=3.5" @@ -4016,7 +4113,7 @@ test = ["pytest"] name = "sphinxcontrib-serializinghtml" version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -category = "dev" +category = "main" optional = false python-versions = ">=3.5" @@ -4132,6 +4229,22 @@ python-versions = ">=3.6" [package.extras] doc = ["reno", "sphinx", "tornado (>=4.5)"] +[[package]] +name = "tensorrt" +version = "8.6.0" +description = "A high performance deep learning inference library" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +nvidia-cublas-cu12 = "*" +nvidia-cuda-runtime-cu12 = "*" +nvidia-cudnn-cu12 = "*" + +[package.extras] +numpy = ["numpy"] + [[package]] name = "terminado" version = "0.16.0" @@ -4260,6 +4373,7 @@ opt-einsum = ["opt-einsum (>=3.3)"] [package.source] type = "url" url = "https://download.pytorch.org/whl/cu118/torch-2.0.0%2Bcu118-cp38-cp38-linux_x86_64.whl" + [[package]] name = "torchsummary" version = "1.5.1" @@ -4288,6 +4402,7 @@ scipy = ["scipy"] [package.source] type = "url" url = "https://download.pytorch.org/whl/cu118/torchvision-0.15.1%2Bcu118-cp38-cp38-linux_x86_64.whl" + [[package]] name = "tornado" version = "6.2" @@ -4343,6 +4458,25 @@ torch = "*" tests = ["autopep8", "flake8", "isort", "numpy", "pytest", "scipy (>=1.7.1)"] tutorials = ["matplotlib", "pandas", "tabulate"] +[[package]] +name = "tritonclient" +version = "2.28.0" +description = "Python client library and utilities for communicating with Triton Inference Server" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +aiohttp = {version = ">=3.8.1", optional = true, markers = "extra == \"http\""} +geventhttpclient = {version = ">=1.4.4,<=2.0.2", optional = true, markers = "extra == \"http\""} +numpy = ">=1.19.1" +python-rapidjson = ">=0.9.1" + +[package.extras] +all = ["aiohttp (>=3.8.1)", "geventhttpclient (>=1.4.4,<=2.0.2)", "grpcio (==1.41.0)", "numpy (>=1.19.1)", "protobuf (>=3.5.0,<3.20)", "python-rapidjson (>=0.9.1)"] +grpc = ["grpcio (==1.41.0)", "numpy (>=1.19.1)", "protobuf (>=3.5.0,<3.20)", "python-rapidjson (>=0.9.1)"] +http = ["aiohttp (>=3.8.1)", "geventhttpclient (>=1.4.4,<=2.0.2)", "numpy (>=1.19.1)", "python-rapidjson (>=0.9.1)"] + [[package]] name = "types-atomicwrites" version = "1.4.5.1" @@ -4606,7 +4740,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "1.1" python-versions = "~3.8" -content-hash = "a16ec604c5251dc860a44f7133289d0b7b16120757f8c903f3917329753c52d1" +content-hash = "774e90b7d2bef68c6d219c8afc3d5717a104a04b9cd7b1b215655eb48fa62d04" [metadata.files] adal = [ @@ -4916,6 +5050,90 @@ breathe = [ {file = "breathe-4.34.0-py3-none-any.whl", hash = "sha256:48804dcf0e607a89fb6ad88c729ef12743a42db03ae9489be4ef8f7c4011774a"}, {file = "breathe-4.34.0.tar.gz", hash = "sha256:ac0768a5e84addad3e632028fe67749c567aba2b29088493b64c2c1634bcdba1"}, ] +brotli = [ + {file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"}, + {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b"}, + {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6"}, + {file = "Brotli-1.0.9-cp27-cp27m-win32.whl", hash = "sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa"}, + {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452"}, + {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7"}, + {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9744a863b489c79a73aba014df554b0e7a0fc44ef3f8a0ef2a52919c7d155031"}, + {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a72661af47119a80d82fa583b554095308d6a4c356b2a554fdc2799bc19f2a43"}, + {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c"}, + {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c"}, + {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0"}, + {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e23281b9a08ec338469268f98f194658abfb13658ee98e2b7f85ee9dd06caa91"}, + {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3496fc835370da351d37cada4cf744039616a6db7d13c430035e901443a34daa"}, + {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83bb06a0192cccf1eb8d0a28672a1b79c74c3a8a5f2619625aeb6f28b3a82bb"}, + {file = "Brotli-1.0.9-cp310-cp310-win32.whl", hash = "sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181"}, + {file = "Brotli-1.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2"}, + {file = "Brotli-1.0.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cc0283a406774f465fb45ec7efb66857c09ffefbe49ec20b7882eff6d3c86d3a"}, + {file = "Brotli-1.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:11d3283d89af7033236fa4e73ec2cbe743d4f6a81d41bd234f24bf63dde979df"}, + {file = "Brotli-1.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c1306004d49b84bd0c4f90457c6f57ad109f5cc6067a9664e12b7b79a9948ad"}, + {file = "Brotli-1.0.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1375b5d17d6145c798661b67e4ae9d5496920d9265e2f00f1c2c0b5ae91fbde"}, + {file = "Brotli-1.0.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cab1b5964b39607a66adbba01f1c12df2e55ac36c81ec6ed44f2fca44178bf1a"}, + {file = "Brotli-1.0.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8ed6a5b3d23ecc00ea02e1ed8e0ff9a08f4fc87a1f58a2530e71c0f48adf882f"}, + {file = "Brotli-1.0.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cb02ed34557afde2d2da68194d12f5719ee96cfb2eacc886352cb73e3808fc5d"}, + {file = "Brotli-1.0.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b3523f51818e8f16599613edddb1ff924eeb4b53ab7e7197f85cbc321cdca32f"}, + {file = "Brotli-1.0.9-cp311-cp311-win32.whl", hash = "sha256:ba72d37e2a924717990f4d7482e8ac88e2ef43fb95491eb6e0d124d77d2a150d"}, + {file = "Brotli-1.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:3ffaadcaeafe9d30a7e4e1e97ad727e4f5610b9fa2f7551998471e3736738679"}, + {file = "Brotli-1.0.9-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4"}, + {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296"}, + {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430"}, + {file = "Brotli-1.0.9-cp35-cp35m-win32.whl", hash = "sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1"}, + {file = "Brotli-1.0.9-cp35-cp35m-win_amd64.whl", hash = "sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea"}, + {file = "Brotli-1.0.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f"}, + {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4"}, + {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a"}, + {file = "Brotli-1.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b"}, + {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:6d847b14f7ea89f6ad3c9e3901d1bc4835f6b390a9c71df999b0162d9bb1e20f"}, + {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:495ba7e49c2db22b046a53b469bbecea802efce200dffb69b93dd47397edc9b6"}, + {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:4688c1e42968ba52e57d8670ad2306fe92e0169c6f3af0089be75bbac0c64a3b"}, + {file = "Brotli-1.0.9-cp36-cp36m-win32.whl", hash = "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14"}, + {file = "Brotli-1.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c"}, + {file = "Brotli-1.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126"}, + {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d"}, + {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12"}, + {file = "Brotli-1.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130"}, + {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7bbff90b63328013e1e8cb50650ae0b9bac54ffb4be6104378490193cd60f85a"}, + {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ec1947eabbaf8e0531e8e899fc1d9876c179fc518989461f5d24e2223395a9e3"}, + {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12effe280b8ebfd389022aa65114e30407540ccb89b177d3fbc9a4f177c4bd5d"}, + {file = "Brotli-1.0.9-cp37-cp37m-win32.whl", hash = "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"}, + {file = "Brotli-1.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5"}, + {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb"}, + {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8"}, + {file = "Brotli-1.0.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb"}, + {file = "Brotli-1.0.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26"}, + {file = "Brotli-1.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c"}, + {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e2d9e1cbc1b25e22000328702b014227737756f4b5bf5c485ac1d8091ada078b"}, + {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b336c5e9cf03c7be40c47b5fd694c43c9f1358a80ba384a21969e0b4e66a9b17"}, + {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85f7912459c67eaab2fb854ed2bc1cc25772b300545fe7ed2dc03954da638649"}, + {file = "Brotli-1.0.9-cp38-cp38-win32.whl", hash = "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429"}, + {file = "Brotli-1.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f"}, + {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19"}, + {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7"}, + {file = "Brotli-1.0.9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b"}, + {file = "Brotli-1.0.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389"}, + {file = "Brotli-1.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7"}, + {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e4c4e92c14a57c9bd4cb4be678c25369bf7a092d55fd0866f759e425b9660806"}, + {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e48f4234f2469ed012a98f4b7874e7f7e173c167bed4934912a29e03167cf6b1"}, + {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ed4c92a0665002ff8ea852353aeb60d9141eb04109e88928026d3c8a9e5433c"}, + {file = "Brotli-1.0.9-cp39-cp39-win32.whl", hash = "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3"}, + {file = "Brotli-1.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761"}, + {file = "Brotli-1.0.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9749a124280a0ada4187a6cfd1ffd35c350fb3af79c706589d98e088c5044267"}, + {file = "Brotli-1.0.9-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:73fd30d4ce0ea48010564ccee1a26bfe39323fde05cb34b5863455629db61dc7"}, + {file = "Brotli-1.0.9-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02177603aaca36e1fd21b091cb742bb3b305a569e2402f1ca38af471777fb019"}, + {file = "Brotli-1.0.9-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:76ffebb907bec09ff511bb3acc077695e2c32bc2142819491579a695f77ffd4d"}, + {file = "Brotli-1.0.9-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b43775532a5904bc938f9c15b77c613cb6ad6fb30990f3b0afaea82797a402d8"}, + {file = "Brotli-1.0.9-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5bf37a08493232fbb0f8229f1824b366c2fc1d02d64e7e918af40acd15f3e337"}, + {file = "Brotli-1.0.9-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:330e3f10cd01da535c70d09c4283ba2df5fb78e915bea0a28becad6e2ac010be"}, + {file = "Brotli-1.0.9-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e1abbeef02962596548382e393f56e4c94acd286bd0c5afba756cffc33670e8a"}, + {file = "Brotli-1.0.9-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3148362937217b7072cf80a2dcc007f09bb5ecb96dae4617316638194113d5be"}, + {file = "Brotli-1.0.9-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:336b40348269f9b91268378de5ff44dc6fbaa2268194f85177b53463d313842a"}, + {file = "Brotli-1.0.9-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b09a16a1950b9ef495a0f8b9d0a87599a9d1f179e2d4ac014b2ec831f87e7"}, + {file = "Brotli-1.0.9-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c8e521a0ce7cf690ca84b8cc2272ddaf9d8a50294fd086da67e517439614c755"}, + {file = "Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"}, +] cachecontrol = [ {file = "CacheControl-0.12.11-py2.py3-none-any.whl", hash = "sha256:2c75d6a8938cb1933c75c50184549ad42728a27e9f6b92fd677c3151aa72555b"}, {file = "CacheControl-0.12.11.tar.gz", hash = "sha256:a5b9fcc986b184db101aa280b42ecdcdfc524892596f606858e0b7a8b4d9e144"}, @@ -5442,6 +5660,14 @@ fastcluster = [ {file = "fastcluster-1.2.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9020899b67fe492d0ed87a3e993ec9962c5a0b51ea2df71d86b1766f065f1cef"}, {file = "fastcluster-1.2.6-cp310-cp310-win32.whl", hash = "sha256:6cf156d4203708348522393c523c2e61c81f5a6a500e0411dcba2b064551ea2f"}, {file = "fastcluster-1.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:1801c9daa9aa5bbbb0830efe8bd3034b4b7a417e4b8dd353683999be29797df2"}, + {file = "fastcluster-1.2.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ce70c743490f6778b463524d1767a9ecccd31c8bd2dbb5739bb2174168c15d39"}, + {file = "fastcluster-1.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac1b84d4b28456a379a71451d13995eb3242143452ce9c861f8913360de842a3"}, + {file = "fastcluster-1.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55b49f6033c45a28f93540847b495ed0f718b5c3f4fef446cf77e3726662e1d5"}, + {file = "fastcluster-1.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1c776a4ec7594f47cd2e1e2da73a30134f1d402d7c93a81e3cb7c3d8e191173"}, + {file = "fastcluster-1.2.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aca61d16435bb7aea3901939d7d7d7e36aff9bb538123e649166a3014b280054"}, + {file = "fastcluster-1.2.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04ea4a68e0675072ca761bad33322a0e998cb43693fd41165bc420d7db40429a"}, + {file = "fastcluster-1.2.6-cp311-cp311-win32.whl", hash = "sha256:773043d5db2790e1ff2a4e1eae0b6a60afb2a93ad2c74897a56c80bc800db04f"}, + {file = "fastcluster-1.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:841d128daa6597d13781793eb482b0b566bbd58d2a9d1e2cf1b58838773beb14"}, {file = "fastcluster-1.2.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cf5acfe1156849279ebd44a8d1fbcbe8b8e21334f7538eda782ae31e7dade9e2"}, {file = "fastcluster-1.2.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb27c13225f5f77f3c5986a27ca27277cce7db12844330cf535019cd38021257"}, {file = "fastcluster-1.2.6-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fe543b6d45da27e84e5af6248722475b88943d8ef40d835cbabbb0ba5ee786b"}, @@ -5700,6 +5926,86 @@ gevent = [ {file = "gevent-22.10.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:0569e133bb620de1001ac807ad9a8abaadedd25349c6d695f80c9048a3f59d42"}, {file = "gevent-22.10.1.tar.gz", hash = "sha256:df3042349c9a4460eeaec8d0e56d737cb183eed055e75a6af9dbda94aaddaf4d"}, ] +geventhttpclient = [ + {file = "geventhttpclient-2.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd76acdc7e7ee5c54c7b279f806b28957a6b092f79c40db34adcfd972749343c"}, + {file = "geventhttpclient-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:320a2c756d8a4f296de370476a1515485c186d9e22c3fc29e04f8f743a7d47bb"}, + {file = "geventhttpclient-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36d3345c6585b09738195a7c45d279a87ccbab0350f1cce3679d3f0dce8577a1"}, + {file = "geventhttpclient-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:407d54499556c2741b93691b86da93232590b013f4a0b773327d766fe3e5c0a9"}, + {file = "geventhttpclient-2.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcf325131b0e4600b793643108cd85dddd66bbf532fd2eb498be5727ef532a1e"}, + {file = "geventhttpclient-2.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5841dd02e6f792a4ef15dbd04fefe620c831ba0b78105808160bb779a31af4"}, + {file = "geventhttpclient-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2ba69422d4e8670dd99803b1313ba574a4d41f52e92b512af51068c9c577bdc1"}, + {file = "geventhttpclient-2.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e3af579c6b46b9caa515a8baf6a2cadeafcd1d41ad22ca5712851f074a40b47"}, + {file = "geventhttpclient-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6ff7fc19f9a4fdd54a2b1c106a705ea2c679fa049685ed763051d417725bdab1"}, + {file = "geventhttpclient-2.0.2-cp310-cp310-win32.whl", hash = "sha256:eec7c52e8eb817674a193e0124486b507215d9e86d34f2638bf9a9292d16f815"}, + {file = "geventhttpclient-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:0e9f7283c01d970e643d89da81127869a8d94bb7a0081020dcad5b590bc007c4"}, + {file = "geventhttpclient-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5ceb492d43a659b895794999dc40d0e7c23b1d41dd34040bbacd0dc264b57d5b"}, + {file = "geventhttpclient-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95959c201d3151fa8f57e0f1ce184476d1173996bdde41dc7d600006023dc5be"}, + {file = "geventhttpclient-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:31c7febba298ecf44838561074a3fb7a01523adca286469b5a82dcc90e8d6a07"}, + {file = "geventhttpclient-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:996c5f453d810b3c592160193d6832a065cca0112e92adc74e62df0e4c564df6"}, + {file = "geventhttpclient-2.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f817e226c02b5a71d86de3772d6accdf250288d1e6825e426c713759830162d"}, + {file = "geventhttpclient-2.0.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c55b7ac0ba0e1e1afbf297b7608f0b3a0bbc34fb4b0c19b7869f32a77ddc6209"}, + {file = "geventhttpclient-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6775bc81e25c48fa58b034444aecfa508b0c3d1bc1e4ae546cc17661be1f51aa"}, + {file = "geventhttpclient-2.0.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a0156882c73537bbbbc7c693ae44c9808119963174078692613ffa4feea21fcf"}, + {file = "geventhttpclient-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3ebb582a291c4c5daaac2ea115b413f4be86874baa60def44d333301cee17bd7"}, + {file = "geventhttpclient-2.0.2-cp311-cp311-win32.whl", hash = "sha256:716f1f72f50b841daf9c9511a01fc31a030866510a11863f27741e26e4f556a7"}, + {file = "geventhttpclient-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:777fcdb72077dfbf70516ecb9e9022246dd337b83a4c1e96f17f3ab9e15f4547"}, + {file = "geventhttpclient-2.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:379d90d8b1fcdda94e74d693806e0b0116c0610504e7f62d5576bac738dc66a5"}, + {file = "geventhttpclient-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00b7b2b836294c091c53789a469c5671202d79420b5191931df4e3a767d607fa"}, + {file = "geventhttpclient-2.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d075355862d7726eb3436f0136fce7650c884f2d04eaae7a39fed3aad9798bc"}, + {file = "geventhttpclient-2.0.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa7b1a27f950d209fe223a97906fe41312dc12c92372424639b8a9b96f1adf91"}, + {file = "geventhttpclient-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fe4e06313aad353b103950780b050d3958000464cc732d621ff8ea3cacbd2bc4"}, + {file = "geventhttpclient-2.0.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:84d7be660b6bc53dd53e3f46b3bc5d275972a8116bd183a77139bb4d9d6d9fb1"}, + {file = "geventhttpclient-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:81f839d6becd664d0972b488422f5dc821f8ad2f2196d53aa5e4d799a3a35a66"}, + {file = "geventhttpclient-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:e707f62271a093e6e3af6f1bbd8cc398b414b8c508fe6b15505dd8e76c4409ac"}, + {file = "geventhttpclient-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:28d7655d1d50bc75ece683a0ae8faf978821d4aeae358d77b59371548db07f1e"}, + {file = "geventhttpclient-2.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58877b4440a580063571a23fbc616aed7c735c6bf9ef525c5129783df8b6966"}, + {file = "geventhttpclient-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57c993c4b2bea551c4a71b75ae1e172e9f3e4352f704ff1b619a0f16aa762f76"}, + {file = "geventhttpclient-2.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3f67e789e31c7b1ce440cd1465dcdefeca29ba6108735eac0b1a593d3a55b7f"}, + {file = "geventhttpclient-2.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f3326e115ec7e7ce95a5d0d47698e8f3584944c4c434a7404937d56b17136b8"}, + {file = "geventhttpclient-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ef328ee3e7dca5055b833fdf3c181647a335abf0249947b27f5df2d95390198c"}, + {file = "geventhttpclient-2.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:27049ea40e3b559eee380310272aaa9b7c19e73c1d9e51e2ec137362be2caa70"}, + {file = "geventhttpclient-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b88a10538341e33fed1682c0dd4579c655d49db5863e7456583085a1cd6bd9d4"}, + {file = "geventhttpclient-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:d52aba2c38420b3fc518188449f1c2a46b1a99adf1c0266c68e72ee0422cd0fa"}, + {file = "geventhttpclient-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:3648626ca58ea4b340e695d78e5d533e6b8be78d375edbd42ff188bc3447e095"}, + {file = "geventhttpclient-2.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fcf96e212b55b93490f3a5fcdfe7a2ef4995a0d13b7d9df398b11e319b7a86b1"}, + {file = "geventhttpclient-2.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e9f2ff09706e3a64a99886d5f2595f3bf364821bc609f2865dbc3e499e21a36"}, + {file = "geventhttpclient-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:721c3075897bfc81e918066f16ae3d1a88c7bb14eeeb831a4f89ea636474643e"}, + {file = "geventhttpclient-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91615fed7931acd49cfe5fc30984acd5411dc1f2643b1544c879d1a537233c6d"}, + {file = "geventhttpclient-2.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7adaa29e5699dea54e0224d1d2d9d8869668d8ad79f5b89433ff9c46f9424a6c"}, + {file = "geventhttpclient-2.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9be5000ba57336a90b438782117c1e43205f51f49aa9b1499a82e210e8431b11"}, + {file = "geventhttpclient-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:12d271cc53486efb3716e99855dc5cb84f2cd3fc9f3243721747bb39ec0fff8a"}, + {file = "geventhttpclient-2.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b9c0c6b75b3905000d2490dc64b4c98a8bac155efbc0ff8917ac082ae0bad261"}, + {file = "geventhttpclient-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e956a457d8831dc81d6f046ab09ebeec680f9a1e9c07e25a1906e77b287918ee"}, + {file = "geventhttpclient-2.0.2-cp38-cp38-win32.whl", hash = "sha256:bc46d5479673dfb293ea428c057d2e23e48ebef5c5d44587cdbaada7f87553e4"}, + {file = "geventhttpclient-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:f44153e4b3ef9b901edcd14be54145a0058bf5fa371b3e583153865fac866245"}, + {file = "geventhttpclient-2.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ebf98db9435824cf0b80b5247be6c88b20bfafd6249f7ebaabb85297da37e380"}, + {file = "geventhttpclient-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c8b7298eb1ebd015257bf4503e34f5fbbe64bd83324140f76b511046aba5a0d5"}, + {file = "geventhttpclient-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:60b81a6d4e65db7c1a5350c9fb72ebf800b478849a7e8020d1ab93af237a3747"}, + {file = "geventhttpclient-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad6c2fcbc3733785bd3b8c2bb43d1f605f9085b0a8b70ce354d198f37143f884"}, + {file = "geventhttpclient-2.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94edb022fa50d576cf63f6dd0c437c1acd24a719872a5935991aaf08f8e88cb2"}, + {file = "geventhttpclient-2.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ca459cedb3827d960362e05ea3a4ae600a6d0d93de77eac2ac0f79828e5e18c"}, + {file = "geventhttpclient-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7551b6db860b56411de1f96618e91b54f65e1a7be8d10255bd1adfb738bb6ee5"}, + {file = "geventhttpclient-2.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bcb7e061c243308d9a44b02de5298001e917f1636a9f270c10da86601fcc8dfa"}, + {file = "geventhttpclient-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:96922d170ef8933f4c20036e8d70d4fbe861f54c543e32e7459ebdbaafa65a2e"}, + {file = "geventhttpclient-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ebb3c993903d40fd4bb1f3e55b84c62c8fc1d14433ae6d4d477dd9a325354c94"}, + {file = "geventhttpclient-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:dbccf1ba155dea3ea99ba0e67a835c05b4303f05298e85f5bb2a46700ccdf092"}, + {file = "geventhttpclient-2.0.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8770b8ab9e8c31d2aaf8a6fbc63fbb7239c58db10bb49cee191ca5c141c61542"}, + {file = "geventhttpclient-2.0.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daff1e977fccf98f27266d3891afdc101f1d705a48331754909e960bcae83f8a"}, + {file = "geventhttpclient-2.0.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2435e0f2a60e00d977822ec4c12e7851deb7aa49a23d32d648e72c641aae3b05"}, + {file = "geventhttpclient-2.0.2-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09acd03d0a8c1bb7d5a1cb6fcb77aaa19a907c1b4915ab58da5d283675edb0a5"}, + {file = "geventhttpclient-2.0.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:5d0813d97050446dab2fb243312e6c446e4ef5e9591befd597ef8f2887f8e2a8"}, + {file = "geventhttpclient-2.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:852da9bb0fc792cdca5ffc9327490094783e42415494b3569e5d532615027439"}, + {file = "geventhttpclient-2.0.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79304a63a9d0512f2757c5862487b332b18a9c85feebecf6ebc3526c6dd1ba2"}, + {file = "geventhttpclient-2.0.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c1c783fce45f16db448d7e34864f1e9c22fe60a7780d2c1c14edbb1fb7262e"}, + {file = "geventhttpclient-2.0.2-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c407c2b4bea817c6f752502db4ab0e9f9465b4fb85b459d1332b5f93a3096c"}, + {file = "geventhttpclient-2.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4f0d70a83ef4ab93102c6601477c13e9cdbc87205e5237fbf5797e30dc9d3ee8"}, + {file = "geventhttpclient-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b03f298ec19b8a4717cce8112fe30322c9e5bfada84dde61a1a44d1eeffc1d3c"}, + {file = "geventhttpclient-2.0.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2dc94b9a23eb6744a8c729aec2b1cdc4e39acf1d8f16ea85a62810aa6b2cae5"}, + {file = "geventhttpclient-2.0.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:805554594bb29231fd990cc2cbbe493d223d76a6085fec891dd76bb4e0928933"}, + {file = "geventhttpclient-2.0.2-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb23527d98f626ca7a4e8961ed9bdc6aed3388de306614c69a133b34262460f4"}, + {file = "geventhttpclient-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a594ab319872a38fb7f16be4cfb107d3c63c43a081f2abe241834e9877f27401"}, + {file = "geventhttpclient-2.0.2.tar.gz", hash = "sha256:8135a85200b170def7293d01dd1557931fcd1bec1ac78c52ad7cedd22368b9ba"}, +] greenlet = [ {file = "greenlet-1.1.3.post0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:949c9061b8c6d3e6e439466a9be1e787208dec6246f4ec5fffe9677b4c19fcc3"}, {file = "greenlet-1.1.3.post0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d7815e1519a8361c5ea2a7a5864945906f8e386fa1bc26797b4d443ab11a4589"}, @@ -6643,6 +6949,17 @@ numpy = [ {file = "numpy-1.23.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4d52914c88b4930dafb6c48ba5115a96cbab40f45740239d9f4159c4ba779962"}, {file = "numpy-1.23.4.tar.gz", hash = "sha256:ed2cc92af0efad20198638c69bb0fc2870a58dabfba6eb722c933b48556c686c"}, ] +nvidia-cublas-cu12 = [ + {file = "nvidia_cublas_cu12-12.1.0.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:4fd00bd12d442f53929616f45ba67552a13d8940f6e766d11d83973a21ada910"}, + {file = "nvidia_cublas_cu12-12.1.0.26-py3-none-win_amd64.whl", hash = "sha256:5c668dcb5cbf49e1add058328300aa90fd012eb958a646a8777d55da2eca5eaa"}, +] +nvidia-cuda-runtime-cu12 = [ + {file = "nvidia_cuda_runtime_cu12-12.1.55-py3-none-manylinux1_x86_64.whl", hash = "sha256:a485693383c7a28ba022587c5a640353ef61a21eb2e87382d0a76340bda92e2e"}, + {file = "nvidia_cuda_runtime_cu12-12.1.55-py3-none-win_amd64.whl", hash = "sha256:6939e48161354dbc096dcc6f7910cb2387227f0bac462c2cff51a7d5b50ad200"}, +] +nvidia-cudnn-cu12 = [ + {file = "nvidia_cudnn_cu12-8.9.0.131-py3-none-manylinux1_x86_64.whl", hash = "sha256:2a95c2e0201187509021270f52971435d171e65627a482b9ebdfdde6749ad485"}, +] nvidia-ml-py3 = [ {file = "nvidia-ml-py3-7.352.0.tar.gz", hash = "sha256:390f02919ee9d73fe63a98c73101061a6b37fa694a793abf56673320f1f51277"}, ] @@ -6679,6 +6996,7 @@ onnx = [ ] onnx2torch = [ {file = "onnx2torch-1.5.4-py3-none-any.whl", hash = "sha256:fd1a0fe05072bfb9f3d86d9330299b130b41f11bd4ae634db17078974e711725"}, + {file = "onnx2torch-1.5.4.tar.gz", hash = "sha256:df837b557a63540223d85fde4a1d679fde0ca8d8bb89d5379c030b01eddc9c24"}, ] onnxoptimizer = [ {file = "onnxoptimizer-0.3.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:e73a5e2e3ca4db9bff54f7131768749c861677b97ee811a136fcf1a52783cf6e"}, @@ -6762,6 +7080,10 @@ pandocfilters = [ {file = "pandocfilters-1.5.0-py2.py3-none-any.whl", hash = "sha256:33aae3f25fd1a026079f5d27bdd52496f0e0803b3469282162bafdcbdf6ef14f"}, {file = "pandocfilters-1.5.0.tar.gz", hash = "sha256:0b679503337d233b4339a817bfc8c50064e2eff681314376a47cb582305a7a38"}, ] +panflute = [ + {file = "panflute-2.3.0-py3-none-any.whl", hash = "sha256:02673bcbdb521a805f08a2ca0ce864de86ad409ad406a01b3700fcf2aca81635"}, + {file = "panflute-2.3.0.tar.gz", hash = "sha256:cefd9dfc48ccd9732a53db57610701d22806da397a8a97e5cc8dc070b55865ca"}, +] parameterized = [ {file = "parameterized-0.8.1-py2.py3-none-any.whl", hash = "sha256:9cbb0b69a03e8695d68b3399a8a5825200976536fe1cb79db60ed6a4c8c9efe9"}, {file = "parameterized-0.8.1.tar.gz", hash = "sha256:41bbff37d6186430f77f900d777e5bb6a24928a1c46fb1de692f8b52b8833b5c"}, @@ -7348,6 +7670,64 @@ python-engineio = [ python-logstash = [ {file = "python-logstash-0.4.8.tar.gz", hash = "sha256:d04e1ce11ecc107e4a4f3b807fc57d96811e964a554081b3bbb44732f74ef5f9"}, ] +python-rapidjson = [ + {file = "python-rapidjson-1.10.tar.gz", hash = "sha256:acfecbf5edb91ec72a20a125de7f56b8c2f6161eff4c65382c8ee6a2484d3540"}, + {file = "python_rapidjson-1.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1db7b0af882999f5685eb7046a0f3b3aca5d55a3e84b3089747d29a4ec6fdade"}, + {file = "python_rapidjson-1.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a87c8c8b615513f9dc414af1554140589036d14840f5e1f1845965e1c0a080e1"}, + {file = "python_rapidjson-1.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0a2f5c4abe529ca2764343416e35710a263832533b7bdc76c3285efb5b5ecc8"}, + {file = "python_rapidjson-1.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40467c3a6d8f070cc4d196fe46a79ed59d1a13a4d3fdc6a0325a21816600e5a7"}, + {file = "python_rapidjson-1.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df4e7237a3e77666ccb9b437013294e6aa3968528f7c61f60f6f38eea0f8f79"}, + {file = "python_rapidjson-1.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:99a5215f24ff1fa6cc67ee275a6852aa56d934d3b8cd7a40197feb632b54fd76"}, + {file = "python_rapidjson-1.10-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3dbea0ee9fa1cd6ecc13a949f6bb94013639d39cdb56f58df4ab61130d35e57c"}, + {file = "python_rapidjson-1.10-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:6d1d0c5da3bd5f701b1aed550e1e7bd59b16ae642877cddf18815006cf998f9a"}, + {file = "python_rapidjson-1.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:072f76c1f1483bcc4056d7d3a8b0319bf841a73e955f188302094b62b2163bf9"}, + {file = "python_rapidjson-1.10-cp310-cp310-win32.whl", hash = "sha256:c95d466307a2140a7687a575103980c6e81c9f62d19556cafad3d6b2932b7eb1"}, + {file = "python_rapidjson-1.10-cp310-cp310-win_amd64.whl", hash = "sha256:454ffda58cc6fed64d983b1b8ae4b39a563b4fd671dae9132e06450025898539"}, + {file = "python_rapidjson-1.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fff343076fbeee0cd7e4e3fb9472f2d567a127ec7b8b5b7ecba6bf7960a3ce07"}, + {file = "python_rapidjson-1.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:686482c67727edad4b6d0c753bc159f35134a5a623e9651c4b7c008ef2996252"}, + {file = "python_rapidjson-1.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ef7d55688b7123d62690b193537cc048fa9f35cfa43d249fedc0d9fd398890a"}, + {file = "python_rapidjson-1.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f27c0601792533ab6e98452961d61566480dc155da19d2a358a5fd9a85d9321"}, + {file = "python_rapidjson-1.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95176e35e3bacb8a1a27f563e815b5b57c717992c871b1c25fd76a835fbba32c"}, + {file = "python_rapidjson-1.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:47a0ec20886b8be86af307c10d699a447e22979ed7dd1f2b7ed5cb7496b3d920"}, + {file = "python_rapidjson-1.10-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f7968c0cb09d9a76aa2483556ba46ab42634baf216cb2f2c7cd6bf77119a33c1"}, + {file = "python_rapidjson-1.10-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5faab270a4dd49216ceaca7169682680b2f5df8311c1ed259e4612d9d0cf61b7"}, + {file = "python_rapidjson-1.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:81b797934dc037810f5f98af138b55a3b6f18dd569cc5e8f81fe79956a4717ca"}, + {file = "python_rapidjson-1.10-cp311-cp311-win32.whl", hash = "sha256:6c1d62cc58a61629fc5e216fb7b3a1b02787c98fded874a7b474b1e6325e377e"}, + {file = "python_rapidjson-1.10-cp311-cp311-win_amd64.whl", hash = "sha256:29d31fc4254f1a4dca420e58bd1331e990fc2959d09ff2daa7934d52732a8491"}, + {file = "python_rapidjson-1.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:718f4e217b511cfbf9166f55ccf4bf4e4538495bee403e390cf89791c0debc26"}, + {file = "python_rapidjson-1.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:541bbb96353cf3fe2bdb29e727087226532be4e4573daad6f042cfdea533a564"}, + {file = "python_rapidjson-1.10-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50f0402a6899c6a177d4a37152deefcd59c61e44bef56b71e8d006a186c86286"}, + {file = "python_rapidjson-1.10-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c035e17744d6d6fba073b550b0040a74e55f2ad33fd798df206ff6879b41ad10"}, + {file = "python_rapidjson-1.10-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da970bde42309a74a5556e696673ea11c4545b8bee5081b84265ded460b2e9ef"}, + {file = "python_rapidjson-1.10-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7437a649821008aa456f2fbec737880d7f9bdda7ec94cc1743a43ccf32b5d26"}, + {file = "python_rapidjson-1.10-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:362d969bbd277f78bf0b1ffaa810857ea40351146b827f896f8d49e9c25fc99c"}, + {file = "python_rapidjson-1.10-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:409256e7748c4ab7f17b3793c7a78ca01914c487644fc42140d116ed4dec8c4f"}, + {file = "python_rapidjson-1.10-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a34a7e2853456fe50ba2ee22e38e7841e55eee10021d4496cce62285f148e8e7"}, + {file = "python_rapidjson-1.10-cp37-cp37m-win32.whl", hash = "sha256:bc4a97940e5afa60a598483d0eb863b26e4810aaf030d92a4301f5fc183e1b6d"}, + {file = "python_rapidjson-1.10-cp37-cp37m-win_amd64.whl", hash = "sha256:89586b67f9c69b66885774acebf3d018e7b8f93cea2b3cffa306ec9d37877594"}, + {file = "python_rapidjson-1.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2e38082b1a8ce3e2bd55821852c0cd643cdabe6497fd9c054f6b47a099afbea0"}, + {file = "python_rapidjson-1.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0955ef22fabb36b26fcad702ae54c1bce2bc2a74b1883c42d251d72011d0d426"}, + {file = "python_rapidjson-1.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a9db03c68ab0158bcdf80299b2c980186d148aa3e05d5650fea5148a425a29a"}, + {file = "python_rapidjson-1.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fde8ab0f06debaa06d93085f19dc3ec3db53f22883f1625dd32b96a87e7009de"}, + {file = "python_rapidjson-1.10-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23539c9f7d85d64a00d3cb44c7d9ab3be2184d4da42a5f3263dcfd1d0203ee43"}, + {file = "python_rapidjson-1.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8bfd484285f3477acef0bb45abd2b80b6252e35a5a53395ce48f0327cbe43c23"}, + {file = "python_rapidjson-1.10-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8b0ed643ebaa8ddf3f40422752efe83abda29aa30a9e6866ccd9dd591b5057d0"}, + {file = "python_rapidjson-1.10-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:5aeca661a0f229f1312fb3ad3e1a5c6736d49942d80d4931810158559eb8f119"}, + {file = "python_rapidjson-1.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8a68ed066e8f0878b7112f943cf35ba9e5217395bcdd8cb478cde01871e2701c"}, + {file = "python_rapidjson-1.10-cp38-cp38-win32.whl", hash = "sha256:d286be6f63446776c4958bb37824c683194b4878fc9cd5b7255134fb5a6ba536"}, + {file = "python_rapidjson-1.10-cp38-cp38-win_amd64.whl", hash = "sha256:aece5270c6e6d5c3d54586c9a5fb9677d70d7019744a59560c5c369c7b9bba25"}, + {file = "python_rapidjson-1.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc09c5ad0fe71f262cdcc5655409f132f1560a8af80e76e7757945ce401fdbab"}, + {file = "python_rapidjson-1.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f6447bd7a8ff5135ab7e372b48a174d3c560d5b322e32bd465e8458e6e4593"}, + {file = "python_rapidjson-1.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22ede69213885391b46cc14596bfd4cd1a5c6f34a2db6600fb08b03982dbc7b7"}, + {file = "python_rapidjson-1.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a9425129623718a04b885a12190faa23e7997c4e8632054e18df7ea473f746d"}, + {file = "python_rapidjson-1.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60e10f32e1a8d155448842934cbe71eb620b4b4a0cb3627ba4c4856e27556534"}, + {file = "python_rapidjson-1.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8d23caab17b87ed5b82e28cdc19172ba1ca65c982e3fff387961d3f33710031f"}, + {file = "python_rapidjson-1.10-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f07d4fcdcfd64bdad0143b9705c5d5089677ebddf60ac6c1f8074a34b1c70cf9"}, + {file = "python_rapidjson-1.10-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:87b991c7ae435489c56a46cef228d2b65a3df689ee4fe24fab69c791c841f633"}, + {file = "python_rapidjson-1.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3706a5c4f21073c04be133c36565efc6e3f5646a03c8d19af78c19d7c70eb708"}, + {file = "python_rapidjson-1.10-cp39-cp39-win32.whl", hash = "sha256:47f9078ea6884f700166a8728d863609fec62232e66a33b8fb4a7706ce7c731c"}, + {file = "python_rapidjson-1.10-cp39-cp39-win_amd64.whl", hash = "sha256:9e4921ab7002ae9faad7f439a7c50aa195039f177e9e51a76c34c97966c79a79"}, +] python-socketio = [ {file = "python-socketio-5.7.2.tar.gz", hash = "sha256:92395062d9db3c13d30e7cdedaa0e1330bba78505645db695415f9a3c628d097"}, {file = "python_socketio-5.7.2-py3-none-any.whl", hash = "sha256:d9a9f047e6fdd306c852fbac36516f4b495c2096f8ad9ceb8803b8e5ff5622e3"}, @@ -7692,6 +8072,7 @@ scons = [ {file = "SCons-4.4.0-py3-none-any.whl", hash = "sha256:cbbd73b83cf85f1aaaf986370359de1bbfd3af97a765cb3554734f6dcec734e1"}, {file = "SCons-4.4.0.tar.gz", hash = "sha256:7703c4e9d2200b4854a31800c1dbd4587e1fa86e75f58795c740bcfa7eca7eaa"}, ] +sconscontrib = [] secretstorage = [ {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, @@ -7721,6 +8102,18 @@ setproctitle = [ {file = "setproctitle-1.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7f2719a398e1a2c01c2a63bf30377a34d0b6ef61946ab9cf4d550733af8f1ef1"}, {file = "setproctitle-1.3.2-cp310-cp310-win32.whl", hash = "sha256:e425be62524dc0c593985da794ee73eb8a17abb10fe692ee43bb39e201d7a099"}, {file = "setproctitle-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:e85e50b9c67854f89635a86247412f3ad66b132a4d8534ac017547197c88f27d"}, + {file = "setproctitle-1.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2a97d51c17d438cf5be284775a322d57b7ca9505bb7e118c28b1824ecaf8aeaa"}, + {file = "setproctitle-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:587c7d6780109fbd8a627758063d08ab0421377c0853780e5c356873cdf0f077"}, + {file = "setproctitle-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d17c8bd073cbf8d141993db45145a70b307385b69171d6b54bcf23e5d644de"}, + {file = "setproctitle-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e932089c35a396dc31a5a1fc49889dd559548d14cb2237adae260382a090382e"}, + {file = "setproctitle-1.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e4f8f12258a8739c565292a551c3db62cca4ed4f6b6126664e2381acb4931bf"}, + {file = "setproctitle-1.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:570d255fd99c7f14d8f91363c3ea96bd54f8742275796bca67e1414aeca7d8c3"}, + {file = "setproctitle-1.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a8e0881568c5e6beff91ef73c0ec8ac2a9d3ecc9edd6bd83c31ca34f770910c4"}, + {file = "setproctitle-1.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4bba3be4c1fabf170595b71f3af46c6d482fbe7d9e0563999b49999a31876f77"}, + {file = "setproctitle-1.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:37ece938110cab2bb3957e3910af8152ca15f2b6efdf4f2612e3f6b7e5459b80"}, + {file = "setproctitle-1.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db684d6bbb735a80bcbc3737856385b55d53f8a44ce9b46e9a5682c5133a9bf7"}, + {file = "setproctitle-1.3.2-cp311-cp311-win32.whl", hash = "sha256:ca58cd260ea02759238d994cfae844fc8b1e206c684beb8f38877dcab8451dfc"}, + {file = "setproctitle-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:88486e6cce2a18a033013d17b30a594f1c5cb42520c49c19e6ade40b864bb7ff"}, {file = "setproctitle-1.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:92c626edc66169a1b09e9541b9c0c9f10488447d8a2b1d87c8f0672e771bc927"}, {file = "setproctitle-1.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:710e16fa3bade3b026907e4a5e841124983620046166f355bbb84be364bf2a02"}, {file = "setproctitle-1.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f29b75e86260b0ab59adb12661ef9f113d2f93a59951373eb6d68a852b13e83"}, @@ -8026,6 +8419,13 @@ tenacity = [ {file = "tenacity-8.1.0-py3-none-any.whl", hash = "sha256:35525cd47f82830069f0d6b73f7eb83bc5b73ee2fff0437952cedf98b27653ac"}, {file = "tenacity-8.1.0.tar.gz", hash = "sha256:e48c437fdf9340f5666b92cd7990e96bc5fc955e1298baf4a907e3972067a445"}, ] +tensorrt = [ + {file = "tensorrt-8.6.0-cp310-none-manylinux_2_17_x86_64.whl", hash = "sha256:8850d470a92e17e65686fce2a5105dc51fc89f2b438d60d66027288a165732ba"}, + {file = "tensorrt-8.6.0-cp36-none-manylinux_2_17_x86_64.whl", hash = "sha256:1eb0b9b29f3f6c9a6f737b95d1edfe63353dd6b5427478b1c580c9cb72340749"}, + {file = "tensorrt-8.6.0-cp37-none-manylinux_2_17_x86_64.whl", hash = "sha256:ae2e99fb98952d71d6a7443335f7f8e1a8c471b8136e33bb491f11e410dea3cd"}, + {file = "tensorrt-8.6.0-cp38-none-manylinux_2_17_x86_64.whl", hash = "sha256:ba9eb8d1a7c32c63981b4203b72b90b38402ad000ec554fb8dd85a7401de60dd"}, + {file = "tensorrt-8.6.0-cp39-none-manylinux_2_17_x86_64.whl", hash = "sha256:4dc971cd8def3b41086c34d93ca8bff56f5d7d9a2ab5f8738307d040b0bf751e"}, +] terminado = [ {file = "terminado-0.16.0-py3-none-any.whl", hash = "sha256:3e995072a7178a104c41134548ce9b03e4e7f0a538e9c29df4f1fbc81c7cfc75"}, {file = "terminado-0.16.0.tar.gz", hash = "sha256:fac14374eb5498bdc157ed32e510b1f60d5c3c7981a9f5ba018bb9a64cec0c25"}, @@ -8090,6 +8490,15 @@ traitlets = [ {file = "traitlets-5.5.0.tar.gz", hash = "sha256:b122f9ff2f2f6c1709dab289a05555be011c87828e911c0cf4074b85cb780a79"}, ] triton = [ + {file = "triton-2.0.0-1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38806ee9663f4b0f7cd64790e96c579374089e58f49aac4a6608121aa55e2505"}, + {file = "triton-2.0.0-1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:226941c7b8595219ddef59a1fdb821e8c744289a132415ddd584facedeb475b1"}, + {file = "triton-2.0.0-1-cp36-cp36m-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4c9fc8c89874bc48eb7e7b2107a9b8d2c0bf139778637be5bfccb09191685cfd"}, + {file = "triton-2.0.0-1-cp37-cp37m-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d2684b6a60b9f174f447f36f933e9a45f31db96cb723723ecd2dcfd1c57b778b"}, + {file = "triton-2.0.0-1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9d4978298b74fcf59a75fe71e535c092b023088933b2f1df933ec32615e4beef"}, + {file = "triton-2.0.0-1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:74f118c12b437fb2ca25e1a04759173b517582fcf4c7be11913316c764213656"}, + {file = "triton-2.0.0-1-pp37-pypy37_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9618815a8da1d9157514f08f855d9e9ff92e329cd81c0305003eb9ec25cc5add"}, + {file = "triton-2.0.0-1-pp38-pypy38_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1aca3303629cd3136375b82cb9921727f804e47ebee27b2677fef23005c3851a"}, + {file = "triton-2.0.0-1-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e3e13aa8b527c9b642e3a9defcc0fbd8ffbe1c80d8ac8c15a01692478dc64d8a"}, {file = "triton-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f05a7e64e4ca0565535e3d5d3405d7e49f9d308505bb7773d21fb26a4c008c2"}, {file = "triton-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb4b99ca3c6844066e516658541d876c28a5f6e3a852286bbc97ad57134827fd"}, {file = "triton-2.0.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47b4d70dc92fb40af553b4460492c31dc7d3a114a979ffb7a5cdedb7eb546c08"}, @@ -8100,6 +8509,10 @@ triton = [ {file = "triton-2.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42a0d2c3fc2eab4ba71384f2e785fbfd47aa41ae05fa58bf12cb31dcbd0aeceb"}, {file = "triton-2.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c47b72c72693198163ece9d90a721299e4fb3b8e24fd13141e384ad952724f"}, ] +tritonclient = [ + {file = "tritonclient-2.28.0-py3-none-any.whl", hash = "sha256:1f58bbe09a88c35f7979de8ab6579a5337372951f723c0aba31e8bee3e8d79da"}, + {file = "tritonclient-2.28.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:47d93197a0876a743012db4c03f1100f7b225b9aaf8d5f8025bf4a5d9e61bfd2"}, +] types-atomicwrites = [ {file = "types-atomicwrites-1.4.5.1.tar.gz", hash = "sha256:9e9f0923ebf93524b28bcece5a23ac8c3820f39b060df29f671936d2e4bc04bc"}, {file = "types_atomicwrites-1.4.5.1-py3-none-any.whl", hash = "sha256:2f1febbdc78b55453b189fa5b136dce34bab7d1d82319163d470e404aab55c83"}, diff --git a/pyproject.toml b/pyproject.toml index 7506ee552d..25b61eb50b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,7 @@ urllib3 = "^1.26.10" utm = "^0.7.0" websocket_client = "^1.3.3" polyline = "^1.4.0" +sconscontrib = {git = "https://github.com/SCons/scons-contrib.git"} [tool.poetry.group.dev.dependencies] @@ -174,6 +175,8 @@ Werkzeug = "^2.1.2" zerorpc = { git = "https://github.com/commaai/zerorpc-python.git", branch = "master" } omegaconf = "^2.3.0" osmnx = "==1.2.2" +tritonclient = {version = "2.28.0", extras = ["http"]} +tensorrt = "^8.6.0" [build-system] diff --git a/release/files_common b/release/files_common index a20f71018b..2fcbf90c91 100644 --- a/release/files_common +++ b/release/files_common @@ -396,9 +396,9 @@ selfdrive/modeld/runners/run.h selfdrive/monitoring/dmonitoringd.py selfdrive/monitoring/driver_monitor.py +selfdrive/navd/.gitignore selfdrive/navd/__init__.py -selfdrive/navd/navd.py -selfdrive/navd/helpers.py +selfdrive/navd/** selfdrive/assets/.gitignore selfdrive/assets/assets.qrc @@ -411,6 +411,7 @@ selfdrive/assets/images/* selfdrive/assets/offroad/* selfdrive/assets/sounds/* selfdrive/assets/training/* +selfdrive/assets/navigation/* third_party/.gitignore third_party/SConscript diff --git a/release/files_tici b/release/files_tici index b4e0c50516..892b7cd2f4 100644 --- a/release/files_tici +++ b/release/files_tici @@ -4,9 +4,6 @@ third_party/mapbox-gl-native-qt/include/* system/timezoned.py -selfdrive/assets/navigation/* -selfdrive/assets/training_wide/* - system/camerad/cameras/camera_qcom2.cc system/camerad/cameras/camera_qcom2.h system/camerad/cameras/camera_util.cc diff --git a/selfdrive/assets/compress-images.sh b/selfdrive/assets/compress-images.sh index 8601b2d61b..a1a4f8bb40 100755 --- a/selfdrive/assets/compress-images.sh +++ b/selfdrive/assets/compress-images.sh @@ -1,7 +1,7 @@ #!/bin/bash echo "compressing training guide images" -optipng -o7 -strip all training/* training_wide/* +optipng -o7 -strip all training/* # This can sometimes provide smaller images -# mogrify -quality 100 -format jpg training_wide/* training/* +# mogrify -quality 100 -format jpg training/* diff --git a/selfdrive/assets/training/step0.png b/selfdrive/assets/training/step0.png index b942703b5d..3c2c5c72a0 100644 Binary files a/selfdrive/assets/training/step0.png and b/selfdrive/assets/training/step0.png differ diff --git a/selfdrive/assets/training/step1.png b/selfdrive/assets/training/step1.png index e2c9f9f60e..0857893118 100644 Binary files a/selfdrive/assets/training/step1.png and b/selfdrive/assets/training/step1.png differ diff --git a/selfdrive/assets/training/step10.png b/selfdrive/assets/training/step10.png index c5ed8fd624..2941316d17 100644 Binary files a/selfdrive/assets/training/step10.png and b/selfdrive/assets/training/step10.png differ diff --git a/selfdrive/assets/training/step11.png b/selfdrive/assets/training/step11.png index 4776593922..7a7c72e3df 100644 Binary files a/selfdrive/assets/training/step11.png and b/selfdrive/assets/training/step11.png differ diff --git a/selfdrive/assets/training/step12.png b/selfdrive/assets/training/step12.png index 497170c978..0d6f64eb84 100644 Binary files a/selfdrive/assets/training/step12.png and b/selfdrive/assets/training/step12.png differ diff --git a/selfdrive/assets/training/step13.png b/selfdrive/assets/training/step13.png index 228d7549d4..565e02fa3f 100644 Binary files a/selfdrive/assets/training/step13.png and b/selfdrive/assets/training/step13.png differ diff --git a/selfdrive/assets/training/step14.png b/selfdrive/assets/training/step14.png index 7f8da0552b..225231cbaa 100644 Binary files a/selfdrive/assets/training/step14.png and b/selfdrive/assets/training/step14.png differ diff --git a/selfdrive/assets/training/step15.png b/selfdrive/assets/training/step15.png index 9aa861c9fa..929c759b26 100644 Binary files a/selfdrive/assets/training/step15.png and b/selfdrive/assets/training/step15.png differ diff --git a/selfdrive/assets/training/step16.png b/selfdrive/assets/training/step16.png index e0b36b0337..161af863aa 100644 Binary files a/selfdrive/assets/training/step16.png and b/selfdrive/assets/training/step16.png differ diff --git a/selfdrive/assets/training/step17.png b/selfdrive/assets/training/step17.png index c6b33c237e..1b0cdb6fbc 100644 Binary files a/selfdrive/assets/training/step17.png and b/selfdrive/assets/training/step17.png differ diff --git a/selfdrive/assets/training/step18.png b/selfdrive/assets/training/step18.png index bd062d4cc0..0e3b64bab5 100644 Binary files a/selfdrive/assets/training/step18.png and b/selfdrive/assets/training/step18.png differ diff --git a/selfdrive/assets/training/step2.png b/selfdrive/assets/training/step2.png index 97c2eb0f4b..55814b8ef9 100644 Binary files a/selfdrive/assets/training/step2.png and b/selfdrive/assets/training/step2.png differ diff --git a/selfdrive/assets/training/step3.png b/selfdrive/assets/training/step3.png index 7489722316..831095b0ae 100644 Binary files a/selfdrive/assets/training/step3.png and b/selfdrive/assets/training/step3.png differ diff --git a/selfdrive/assets/training/step4.png b/selfdrive/assets/training/step4.png index 8139349ff7..5433034939 100644 Binary files a/selfdrive/assets/training/step4.png and b/selfdrive/assets/training/step4.png differ diff --git a/selfdrive/assets/training/step5.png b/selfdrive/assets/training/step5.png index 714162ae1f..7191b63a0c 100644 Binary files a/selfdrive/assets/training/step5.png and b/selfdrive/assets/training/step5.png differ diff --git a/selfdrive/assets/training/step6.png b/selfdrive/assets/training/step6.png index 356d76a3e8..8eafd4a198 100644 Binary files a/selfdrive/assets/training/step6.png and b/selfdrive/assets/training/step6.png differ diff --git a/selfdrive/assets/training/step7.png b/selfdrive/assets/training/step7.png index ac09faffe8..502f5f1b2e 100644 Binary files a/selfdrive/assets/training/step7.png and b/selfdrive/assets/training/step7.png differ diff --git a/selfdrive/assets/training/step8.png b/selfdrive/assets/training/step8.png index f081ac6e45..c4e8668332 100644 Binary files a/selfdrive/assets/training/step8.png and b/selfdrive/assets/training/step8.png differ diff --git a/selfdrive/assets/training/step9.png b/selfdrive/assets/training/step9.png index 540dafe787..84eae3a066 100644 Binary files a/selfdrive/assets/training/step9.png and b/selfdrive/assets/training/step9.png differ diff --git a/selfdrive/assets/training_wide/step0.png b/selfdrive/assets/training_wide/step0.png deleted file mode 100644 index 3c2c5c72a0..0000000000 Binary files a/selfdrive/assets/training_wide/step0.png and /dev/null differ diff --git a/selfdrive/assets/training_wide/step1.png b/selfdrive/assets/training_wide/step1.png deleted file mode 100644 index 0857893118..0000000000 Binary files a/selfdrive/assets/training_wide/step1.png and /dev/null differ diff --git a/selfdrive/assets/training_wide/step10.png b/selfdrive/assets/training_wide/step10.png deleted file mode 100644 index 2941316d17..0000000000 Binary files a/selfdrive/assets/training_wide/step10.png and /dev/null differ diff --git a/selfdrive/assets/training_wide/step11.png b/selfdrive/assets/training_wide/step11.png deleted file mode 100644 index 7a7c72e3df..0000000000 Binary files a/selfdrive/assets/training_wide/step11.png and /dev/null differ diff --git a/selfdrive/assets/training_wide/step12.png b/selfdrive/assets/training_wide/step12.png deleted file mode 100644 index 0d6f64eb84..0000000000 Binary files a/selfdrive/assets/training_wide/step12.png and /dev/null differ diff --git a/selfdrive/assets/training_wide/step13.png b/selfdrive/assets/training_wide/step13.png deleted file mode 100644 index 565e02fa3f..0000000000 Binary files a/selfdrive/assets/training_wide/step13.png and /dev/null differ diff --git a/selfdrive/assets/training_wide/step14.png b/selfdrive/assets/training_wide/step14.png deleted file mode 100644 index 225231cbaa..0000000000 Binary files a/selfdrive/assets/training_wide/step14.png and /dev/null differ diff --git a/selfdrive/assets/training_wide/step15.png b/selfdrive/assets/training_wide/step15.png deleted file mode 100644 index 929c759b26..0000000000 Binary files a/selfdrive/assets/training_wide/step15.png and /dev/null differ diff --git a/selfdrive/assets/training_wide/step16.png b/selfdrive/assets/training_wide/step16.png deleted file mode 100644 index 161af863aa..0000000000 Binary files a/selfdrive/assets/training_wide/step16.png and /dev/null differ diff --git a/selfdrive/assets/training_wide/step17.png b/selfdrive/assets/training_wide/step17.png deleted file mode 100644 index 1b0cdb6fbc..0000000000 Binary files a/selfdrive/assets/training_wide/step17.png and /dev/null differ diff --git a/selfdrive/assets/training_wide/step18.png b/selfdrive/assets/training_wide/step18.png deleted file mode 100644 index 0e3b64bab5..0000000000 Binary files a/selfdrive/assets/training_wide/step18.png and /dev/null differ diff --git a/selfdrive/assets/training_wide/step2.png b/selfdrive/assets/training_wide/step2.png deleted file mode 100644 index 55814b8ef9..0000000000 Binary files a/selfdrive/assets/training_wide/step2.png and /dev/null differ diff --git a/selfdrive/assets/training_wide/step3.png b/selfdrive/assets/training_wide/step3.png deleted file mode 100644 index 831095b0ae..0000000000 Binary files a/selfdrive/assets/training_wide/step3.png and /dev/null differ diff --git a/selfdrive/assets/training_wide/step4.png b/selfdrive/assets/training_wide/step4.png deleted file mode 100644 index 5433034939..0000000000 Binary files a/selfdrive/assets/training_wide/step4.png and /dev/null differ diff --git a/selfdrive/assets/training_wide/step5.png b/selfdrive/assets/training_wide/step5.png deleted file mode 100644 index 7191b63a0c..0000000000 Binary files a/selfdrive/assets/training_wide/step5.png and /dev/null differ diff --git a/selfdrive/assets/training_wide/step6.png b/selfdrive/assets/training_wide/step6.png deleted file mode 100644 index 8eafd4a198..0000000000 Binary files a/selfdrive/assets/training_wide/step6.png and /dev/null differ diff --git a/selfdrive/assets/training_wide/step7.png b/selfdrive/assets/training_wide/step7.png deleted file mode 100644 index 502f5f1b2e..0000000000 Binary files a/selfdrive/assets/training_wide/step7.png and /dev/null differ diff --git a/selfdrive/assets/training_wide/step8.png b/selfdrive/assets/training_wide/step8.png deleted file mode 100644 index c4e8668332..0000000000 Binary files a/selfdrive/assets/training_wide/step8.png and /dev/null differ diff --git a/selfdrive/assets/training_wide/step9.png b/selfdrive/assets/training_wide/step9.png deleted file mode 100644 index 84eae3a066..0000000000 Binary files a/selfdrive/assets/training_wide/step9.png and /dev/null differ diff --git a/selfdrive/boardd/SConscript b/selfdrive/boardd/SConscript index d99e67a9f0..2fe4591484 100644 --- a/selfdrive/boardd/SConscript +++ b/selfdrive/boardd/SConscript @@ -1,9 +1,11 @@ Import('env', 'envCython', 'common', 'cereal', 'messaging') libs = ['usb-1.0', common, cereal, messaging, 'pthread', 'zmq', 'capnp', 'kj'] -env.Program('boardd', ['main.cc', 'boardd.cc', 'panda.cc', 'panda_comms.cc', 'spi.cc'], LIBS=libs) +panda = env.Library('panda', ['panda.cc', 'panda_comms.cc', 'spi.cc']) + +env.Program('boardd', ['main.cc', 'boardd.cc'], LIBS=[panda] + libs) env.Library('libcan_list_to_can_capnp', ['can_list_to_can_capnp.cc']) envCython.Program('boardd_api_impl.so', 'boardd_api_impl.pyx', LIBS=["can_list_to_can_capnp", 'capnp', 'kj'] + envCython["LIBS"]) if GetOption('test'): - env.Program('tests/test_boardd_usbprotocol', ['tests/test_boardd_usbprotocol.cc', 'panda.cc', 'panda_comms.cc', 'spi.cc'], LIBS=libs) + env.Program('tests/test_boardd_usbprotocol', ['tests/test_boardd_usbprotocol.cc'], LIBS=[panda] + libs) diff --git a/selfdrive/boardd/boardd.cc b/selfdrive/boardd/boardd.cc index 5d885c2c79..e56e1c2175 100644 --- a/selfdrive/boardd/boardd.cc +++ b/selfdrive/boardd/boardd.cc @@ -113,32 +113,35 @@ bool safety_setter_thread(std::vector pandas) { return false; } - // set to ELM327 for fingerprinting + // initialize to ELM327 without OBD multiplexing for fingerprinting + bool obd_multiplexing_enabled = false; for (int i = 0; i < pandas.size(); i++) { - const uint16_t safety_param = (i > 0) ? 1U : 0U; - pandas[i]->set_safety_model(cereal::CarParams::SafetyModel::ELM327, safety_param); + pandas[i]->set_safety_model(cereal::CarParams::SafetyModel::ELM327, 1U); } - // wait for FW query at OBD port to finish + // openpilot can switch between multiplexing modes for different FW queries while (true) { if (do_exit || !check_all_connected(pandas) || !ignition) { return false; } - if (p.getBool("FirmwareObdQueryDone")) { - LOGW("finished FW query at OBD port"); + bool obd_multiplexing_requested = p.getBool("ObdMultiplexingEnabled"); + if (obd_multiplexing_requested != obd_multiplexing_enabled) { + for (int i = 0; i < pandas.size(); i++) { + const uint16_t safety_param = (i > 0 || !obd_multiplexing_requested) ? 1U : 0U; + pandas[i]->set_safety_model(cereal::CarParams::SafetyModel::ELM327, safety_param); + } + obd_multiplexing_enabled = obd_multiplexing_requested; + p.putBool("ObdMultiplexingChanged", true); + } + + if (p.getBool("FirmwareQueryDone")) { + LOGW("finished FW query"); break; } util::sleep_for(20); } - // set to ELM327 to finish fingerprinting and for potential ECU knockouts - for (Panda *panda : pandas) { - panda->set_safety_model(cereal::CarParams::SafetyModel::ELM327, 1U); - } - - p.putBool("ObdMultiplexingDisabled", true); - std::string params; LOGW("waiting for params to set safety model"); while (true) { @@ -360,6 +363,8 @@ std::optional send_panda_states(PubMaster *pm, const std::vector } auto ps = pss[i]; + ps.setVoltage(health.voltage_pkt); + ps.setCurrent(health.current_pkt); ps.setUptime(health.uptime_pkt); ps.setSafetyTxBlocked(health.safety_tx_blocked_pkt); ps.setSafetyRxInvalid(health.safety_rx_invalid_pkt); @@ -380,7 +385,9 @@ std::optional send_panda_states(PubMaster *pm, const std::vector ps.setHarnessStatus(cereal::PandaState::HarnessStatus(health.car_harness_status_pkt)); ps.setInterruptLoad(health.interrupt_load); ps.setFanPower(health.fan_power); + ps.setFanStallCount(health.fan_stall_count); ps.setSafetyRxChecksInvalid((bool)(health.safety_rx_checks_invalid)); + ps.setSpiChecksumErrorCount(health.spi_checksum_error_count); std::array cs = {ps.initCanState0(), ps.initCanState1(), ps.initCanState2()}; @@ -415,7 +422,7 @@ std::optional send_panda_states(PubMaster *pm, const std::vector size_t j = 0; for (size_t f = size_t(cereal::PandaState::FaultType::RELAY_MALFUNCTION); - f <= size_t(cereal::PandaState::FaultType::INTERRUPT_RATE_EXTI); f++) { + f <= size_t(cereal::PandaState::FaultType::HEARTBEAT_LOOP_WATCHDOG); f++) { if (fault_bits.test(f)) { faults.set(j, cereal::PandaState::FaultType(f)); j++; diff --git a/selfdrive/boardd/panda.cc b/selfdrive/boardd/panda.cc index 647a0d9c78..4873040f37 100644 --- a/selfdrive/boardd/panda.cc +++ b/selfdrive/boardd/panda.cc @@ -21,9 +21,6 @@ Panda::Panda(std::string serial, uint32_t bus_offset) : bus_offset(bus_offset) { hw_type = get_hw_type(); - assert((hw_type != cereal::PandaState::PandaType::WHITE_PANDA) && - (hw_type != cereal::PandaState::PandaType::GREY_PANDA)); - has_rtc = (hw_type == cereal::PandaState::PandaType::UNO) || (hw_type == cereal::PandaState::PandaType::DOS) || (hw_type == cereal::PandaState::PandaType::TRES); @@ -203,7 +200,7 @@ void Panda::pack_can_buffer(const capnp::List::Reader &can_data assert(can_data.size() <= 64); assert(can_data.size() == dlc_to_len[data_len_code]); - can_header header; + can_header header = {}; header.addr = cmsg.getAddress(); header.extended = (cmsg.getAddress() >= 0x800) ? 1 : 0; header.data_len_code = data_len_code; diff --git a/selfdrive/boardd/pandad.py b/selfdrive/boardd/pandad.py index f292b921cf..f0ab76e1dd 100755 --- a/selfdrive/boardd/pandad.py +++ b/selfdrive/boardd/pandad.py @@ -115,6 +115,14 @@ def main() -> NoReturn: cloudlog.info(f"Resetting panda {panda.get_usb_serial()}") panda.reset() + # Ensure internal panda is present if expected + internal_pandas = [panda for panda in pandas if panda.is_internal()] + if HARDWARE.has_internal_panda() and len(internal_pandas) == 0: + cloudlog.error("Internal panda is missing, resetting") + HARDWARE.reset_internal_panda() + time.sleep(2) # wait to come back up + continue + # sort pandas to have deterministic order pandas.sort(key=cmp_to_key(panda_sort_cmp)) panda_serials = list(map(lambda p: p.get_usb_serial(), pandas)) # type: ignore diff --git a/selfdrive/boardd/set_time.py b/selfdrive/boardd/set_time.py index 7d17be4de9..2159eba5eb 100755 --- a/selfdrive/boardd/set_time.py +++ b/selfdrive/boardd/set_time.py @@ -1,11 +1,9 @@ #!/usr/bin/env python3 -import datetime import os -import struct -import usb1 +import datetime +from panda import Panda -REQUEST_IN = usb1.ENDPOINT_IN | usb1.TYPE_VENDOR | usb1.RECIPIENT_DEVICE -MIN_DATE = datetime.datetime(year=2021, month=4, day=1) +MIN_DATE = datetime.datetime(year=2023, month=4, day=1) def set_time(logger): sys_time = datetime.datetime.today() @@ -14,24 +12,27 @@ def set_time(logger): return try: - ctx = usb1.USBContext() - dev = ctx.openByVendorIDAndProductID(0xbbaa, 0xddcc) - if dev is None: - logger.info("No panda found") + ps = Panda.list() + if len(ps) == 0: + logger.error("Failed to set time, no pandas found") return - # Set system time from panda RTC time - dat = dev.controlRead(REQUEST_IN, 0xa0, 0, 0, 8) - a = struct.unpack("HBBBBBB", dat) - panda_time = datetime.datetime(a[0], a[1], a[2], a[4], a[5], a[6]) - if panda_time > MIN_DATE: - logger.info(f"adjusting time from '{sys_time}' to '{panda_time}'") - os.system(f"TZ=UTC date -s '{panda_time}'") + for s in ps: + with Panda(serial=s) as p: + if not p.is_internal(): + continue + + # Set system time from panda RTC time + panda_time = p.get_datetime() + if panda_time > MIN_DATE: + logger.info(f"adjusting time from '{sys_time}' to '{panda_time}'") + os.system(f"TZ=UTC date -s '{panda_time}'") + break except Exception: - logger.warn("Failed to fetch time from panda") + logger.exception("Failed to fetch time from panda") if __name__ == "__main__": import logging - logging.basicConfig(level=logging.INFO) + logging.basicConfig(level=logging.DEBUG) set_time(logging) diff --git a/selfdrive/boardd/spi.cc b/selfdrive/boardd/spi.cc index 9a10e30f95..d418d2bdac 100644 --- a/selfdrive/boardd/spi.cc +++ b/selfdrive/boardd/spi.cc @@ -57,9 +57,12 @@ PandaSpiHandle::PandaSpiHandle(std::string serial) : PandaCommsHandle(serial) { uint8_t uid[uid_len] = {0}; uint32_t spi_mode = SPI_MODE_0; - uint32_t spi_speed = 30000000; uint8_t spi_bits_per_word = 8; + // 50MHz is the max of the 845. note that some older + // revs of the comma three may not support this speed + uint32_t spi_speed = 50000000; + spi_fd = open(SPI_DEVICE.c_str(), O_RDWR); if (spi_fd < 0) { LOGE("failed opening SPI device %d", spi_fd); diff --git a/selfdrive/boardd/tests/test_boardd_loopback.py b/selfdrive/boardd/tests/test_boardd_loopback.py index b8ebbd88a3..6217561bd1 100755 --- a/selfdrive/boardd/tests/test_boardd_loopback.py +++ b/selfdrive/boardd/tests/test_boardd_loopback.py @@ -40,8 +40,9 @@ class TestBoardd(unittest.TestCase): sm.update(1000) num_pandas = len(sm['pandaStates']) - if TICI: - self.assertGreater(num_pandas, 1, "connect another panda for multipanda tests") + expected_pandas = 2 if TICI and "SINGLE_PANDA" not in os.environ else 1 + self.assertEqual(num_pandas, expected_pandas, "connected pandas ({num_pandas}) doesn't match expected panda count ({expected_pandas}). \ + connect another panda for multipanda tests.") # boardd blocks on CarVin and CarParams cp = car.CarParams.new_message() @@ -51,7 +52,7 @@ class TestBoardd(unittest.TestCase): cp.safetyConfigs = [safety_config]*num_pandas params = Params() - params.put_bool("FirmwareObdQueryDone", True) + params.put_bool("FirmwareQueryDone", True) params.put_bool("ControlsReady", True) params.put("CarParams", cp.to_bytes()) diff --git a/selfdrive/boardd/tests/test_pandad.py b/selfdrive/boardd/tests/test_pandad.py index 3b2369b39b..09dba6ec7a 100755 --- a/selfdrive/boardd/tests/test_pandad.py +++ b/selfdrive/boardd/tests/test_pandad.py @@ -4,9 +4,11 @@ import unittest import cereal.messaging as messaging from panda import Panda +from common.gpio import gpio_set, gpio_init from selfdrive.test.helpers import phone_only from selfdrive.manager.process_config import managed_processes from system.hardware import HARDWARE +from system.hardware.tici.pins import GPIO class TestPandad(unittest.TestCase): @@ -14,9 +16,9 @@ class TestPandad(unittest.TestCase): def tearDown(self): managed_processes['pandad'].stop() - def _wait_for_boardd(self): + def _wait_for_boardd(self, timeout=30): sm = messaging.SubMaster(['peripheralState']) - for _ in range(30): + for _ in range(timeout): sm.update(1000) if sm.updated['peripheralState']: break @@ -30,7 +32,7 @@ class TestPandad(unittest.TestCase): time.sleep(1) managed_processes['pandad'].start() - self._wait_for_boardd() + self._wait_for_boardd(60) @phone_only def test_in_bootstub(self): @@ -40,6 +42,18 @@ class TestPandad(unittest.TestCase): managed_processes['pandad'].start() self._wait_for_boardd() + @phone_only + def test_internal_panda_reset(self): + gpio_init(GPIO.STM_RST_N, True) + gpio_set(GPIO.STM_RST_N, 1) + time.sleep(0.5) + assert all(not Panda(s).is_internal() for s in Panda.list()) + + managed_processes['pandad'].start() + self._wait_for_boardd() + + assert any(Panda(s).is_internal() for s in Panda.list()) + #def test_out_of_date_fw(self): # pass diff --git a/selfdrive/car/CARS_template.md b/selfdrive/car/CARS_template.md index 1c80ea3d93..8a879954fa 100644 --- a/selfdrive/car/CARS_template.md +++ b/selfdrive/car/CARS_template.md @@ -6,7 +6,7 @@ # Supported Cars -A supported vehicle is one that just works when you install a comma three. All supported cars provide a better experience than any stock system. +A supported vehicle is one that just works when you install a comma three. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified. # {{all_car_info | length}} Supported Cars diff --git a/selfdrive/car/body/interface.py b/selfdrive/car/body/interface.py index 850a3538ad..4d583badae 100644 --- a/selfdrive/car/body/interface.py +++ b/selfdrive/car/body/interface.py @@ -8,7 +8,7 @@ from selfdrive.car.body.values import SPEED_FROM_RPM class CarInterface(CarInterfaceBase): @staticmethod - def _get_params(ret, candidate, fingerprint, car_fw, experimental_long): + def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs): ret.notCar = True ret.carName = "body" ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.body)] diff --git a/selfdrive/car/car_helpers.py b/selfdrive/car/car_helpers.py index dc82f56197..3d8ea22ef2 100644 --- a/selfdrive/car/car_helpers.py +++ b/selfdrive/car/car_helpers.py @@ -8,7 +8,7 @@ from system.version import is_comma_remote, is_tested_branch from selfdrive.car.interfaces import get_interface_attr from selfdrive.car.fingerprints import eliminate_incompatible_cars, all_legacy_fingerprint_cars from selfdrive.car.vin import get_vin, is_valid_vin, VIN_UNKNOWN -from selfdrive.car.fw_versions import disable_obd_multiplexing, get_fw_versions_ordered, match_fw_to_car, get_present_ecus +from selfdrive.car.fw_versions import get_fw_versions_ordered, get_present_ecus, match_fw_to_car, set_obd_multiplexing from system.swaglog import cloudlog import cereal.messaging as messaging from selfdrive.car import gen_empty_fingerprint @@ -80,12 +80,13 @@ def fingerprint(logcan, sendcan, num_pandas): fixed_fingerprint = os.environ.get('FINGERPRINT', "") skip_fw_query = os.environ.get('SKIP_FW_QUERY', False) ecu_rx_addrs = set() + params = Params() if not skip_fw_query: # Vin query only reliably works through OBDII bus = 1 - cached_params = Params().get("CarParamsCache") + cached_params = params.get("CarParamsCache") if cached_params is not None: cached_params = car.CarParams.from_bytes(cached_params) if cached_params.carName == "mock": @@ -98,6 +99,7 @@ def fingerprint(logcan, sendcan, num_pandas): cached = True else: cloudlog.warning("Getting VIN & FW versions") + set_obd_multiplexing(params, True) vin_rx_addr, vin = get_vin(logcan, sendcan, bus) ecu_rx_addrs = get_present_ecus(logcan, sendcan, num_pandas=num_pandas) car_fw = get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, num_pandas=num_pandas) @@ -113,10 +115,11 @@ def fingerprint(logcan, sendcan, num_pandas): cloudlog.event("Malformed VIN", vin=vin, error=True) vin = VIN_UNKNOWN cloudlog.warning("VIN %s", vin) - - params = Params() params.put("CarVin", vin) - disable_obd_multiplexing(params) + + # disable OBD multiplexing for potential ECU knockouts + set_obd_multiplexing(params, False) + params.put_bool("FirmwareQueryDone", True) finger = gen_empty_fingerprint() candidate_cars = {i: all_legacy_fingerprint_cars() for i in [0, 1]} # attempt fingerprint on both bus 0 and 1 @@ -184,7 +187,7 @@ def get_car(logcan, sendcan, experimental_long_allowed, num_pandas=1): candidate = "mock" CarInterface, CarController, CarState = interfaces[candidate] - CP = CarInterface.get_params(candidate, fingerprints, car_fw, experimental_long_allowed) + CP = CarInterface.get_params(candidate, fingerprints, car_fw, experimental_long_allowed, docs=False) CP.carVin = vin CP.carFw = car_fw CP.fingerprintSource = source diff --git a/selfdrive/car/chrysler/interface.py b/selfdrive/car/chrysler/interface.py index 303f563c90..22b2073883 100755 --- a/selfdrive/car/chrysler/interface.py +++ b/selfdrive/car/chrysler/interface.py @@ -8,7 +8,7 @@ from selfdrive.car.interfaces import CarInterfaceBase class CarInterface(CarInterfaceBase): @staticmethod - def _get_params(ret, candidate, fingerprint, car_fw, experimental_long): + def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs): ret.carName = "chrysler" ret.dashcamOnly = candidate in RAM_HD diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index 9f00484f34..8799f073c9 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -50,6 +50,7 @@ class CarControllerParams: self.STEER_DELTA_DOWN = 3 self.STEER_MAX = 261 # higher than this faults the EPS + STEER_THRESHOLD = 120 RAM_DT = {CAR.RAM_1500, } @@ -62,6 +63,7 @@ class ChryslerCarInfo(CarInfo): package: str = "Adaptive Cruise Control (ACC)" harness: Enum = Harness.fca + CAR_INFO: Dict[str, Optional[Union[ChryslerCarInfo, List[ChryslerCarInfo]]]] = { CAR.PACIFICA_2017_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2017-18"), CAR.PACIFICA_2018_HYBRID: None, # same platforms @@ -73,10 +75,10 @@ CAR_INFO: Dict[str, Optional[Union[ChryslerCarInfo, List[ChryslerCarInfo]]]] = { ], CAR.JEEP_CHEROKEE: ChryslerCarInfo("Jeep Grand Cherokee 2016-18", video_link="https://www.youtube.com/watch?v=eLR9o2JkuRk"), CAR.JEEP_CHEROKEE_2019: ChryslerCarInfo("Jeep Grand Cherokee 2019-21", video_link="https://www.youtube.com/watch?v=jBe4lWnRSu4"), - CAR.RAM_1500: ChryslerCarInfo("Ram 1500 2019-22", harness=Harness.ram), + CAR.RAM_1500: ChryslerCarInfo("Ram 1500 2019-23", harness=Harness.ram), CAR.RAM_HD: [ ChryslerCarInfo("Ram 2500 2020-22", harness=Harness.ram), - ChryslerCarInfo("Ram 3500 2020-22", harness=Harness.ram), + ChryslerCarInfo("Ram 3500 2019-22", harness=Harness.ram), ], } @@ -180,79 +182,153 @@ FW_QUERY_CONFIG = FwQueryConfig( ) FW_VERSIONS = { + CAR.JEEP_CHEROKEE_2019: { + (Ecu.combinationMeter, 0x742, None): [ + b'68402971AD', + ], + (Ecu.srs, 0x744, None): [ + b'68355363AB', + ], + (Ecu.abs, 0x747, None): [ + b'68408639AD', + ], + (Ecu.fwdRadar, 0x753, None): [ + b'68456722AC', + ], + (Ecu.eps, 0x75A, None): [ + b'68453431AA', + ], + (Ecu.engine, 0x7e0, None): [ + b'05035674AB ', + ], + (Ecu.transmission, 0x7e1, None): [ + b'05035707AA', + ], + }, + CAR.RAM_1500: { (Ecu.combinationMeter, 0x742, None): [ - b'68294063AH', + b'68294051AG', + b'68294051AI', + b'68294052AG', b'68294063AG', + b'68294063AH', + b'68294063AI', + b'68434846AC', + b'68434858AC', b'68434860AC', - b'68527375AD', b'68453503AC', + b'68453505AC', + b'68453511AC', + b'68453513AD', + b'68453514AD', + b'68510283AG', + b'68527375AD', ], (Ecu.srs, 0x744, None): [ + b'68428609AB', b'68441329AB', + b'68473844AB', b'68490898AA', - b'68428609AB', b'68500728AA', + b'68615033AA', ], (Ecu.abs, 0x747, None): [ - b'68432418AD', + b'68292406AH', b'68432418AB', + b'68432418AD', + b'68436004AD', b'68436004AE', + b'68438454AC', b'68438454AD', - b'68436004AD', + b'68438456AE', + b'68438456AF', b'68535469AB', - b'68438454AC', + b'68535470AC', + b'68586307AB', ], (Ecu.fwdRadar, 0x753, None): [ - b'68320950AL', + b'04672892AB', + b'04672932AB', + b'68320950AH', + b'68320950AI', b'68320950AJ', + b'68320950AL', + b'68320950AM', b'68454268AB', - b'68475160AG', - b'04672892AB', b'68475160AE', + b'68475160AF', + b'68475160AG', ], (Ecu.eps, 0x75A, None): [ + b'68273275AF', b'68273275AG', + b'68312176AE', + b'68312176AG', + b'68440789AC', + b'68466110AB', b'68469901AA', + b'68522583AB', + b'68522585AB', b'68552788AA', + b'68552790AA', + b'68585112AB', ], (Ecu.engine, 0x7e0, None): [ + b'05036065AE ', + b'05036066AE ', + b'68378701AI ', + b'68378758AM ', b'68448163AJ', + b'68448165AK', b'68500630AD', + b'68500630AE', b'68539650AD', - b'68378758AM ', ], (Ecu.transmission, 0x7e1, None): [ b'68360078AL', - b'68384328AD', - b'68360085AL', + b'68360080AM', b'68360081AM', - b'68502994AD', + b'68360085AL', + b'68384328AD', + b'68384332AD', b'68445533AB', - b'68540431AB', b'68484467AC', + b'68502994AD', + b'68540431AB', ], }, CAR.RAM_HD: { (Ecu.combinationMeter, 0x742, None): [ b'68361606AH', + b'68437735AC', b'68492693AD', + b'68525485AB', + b'68525487AB', + b'68525498AB', ], (Ecu.srs, 0x744, None): [ b'68399794AC', b'68428503AA', b'68428505AA', + b'68428507AA', ], (Ecu.abs, 0x747, None): [ b'68334977AH', + b'68455481AC', + b'68504022AA', b'68504022AB', - b'68530686AB', b'68504022AC', + b'68530686AB', + b'68530686AC', ], (Ecu.fwdRadar, 0x753, None): [ b'04672895AB', b'56029827AG', + b'56029827AH', + b'68462657AE', + b'68484694AD', b'68484694AE', ], (Ecu.eps, 0x761, None): [ @@ -260,7 +336,13 @@ FW_VERSIONS = { b'68507906AB', ], (Ecu.engine, 0x7e0, None): [ + b'52370131AF', + b'52370231AF', + b'52370231AG', + b'52370931CT', + b'52401032AE', b'52421132AF', + b'68527616AD ', b'M2370131MB', b'M2421132MB', ], diff --git a/selfdrive/car/docs.py b/selfdrive/car/docs.py index bc03619d0d..74fd4616fd 100755 --- a/selfdrive/car/docs.py +++ b/selfdrive/car/docs.py @@ -29,7 +29,7 @@ def get_all_car_info() -> List[CarInfo]: all_car_info: List[CarInfo] = [] footnotes = get_all_footnotes() for model, car_info in get_interface_attr("CAR_INFO", combine_brands=True).items(): - CP = interfaces[model][0].get_params(model, fingerprint=gen_empty_fingerprint(), car_fw=[car.CarParams.CarFw(ecu="unknown")], experimental_long=False) + CP = interfaces[model][0].get_params(model, fingerprint=gen_empty_fingerprint(), car_fw=[car.CarParams.CarFw(ecu="unknown")], experimental_long=False, docs=True) if CP.dashcamOnly or car_info is None: continue diff --git a/selfdrive/car/docs_definitions.py b/selfdrive/car/docs_definitions.py index c0fb4420df..fa95b03fd8 100644 --- a/selfdrive/car/docs_definitions.py +++ b/selfdrive/car/docs_definitions.py @@ -117,8 +117,20 @@ def split_name(name: str) -> Tuple[str, str, str]: @dataclass class CarInfo: + # make + model + model years name: str + + # Example for Toyota Corolla MY20 + # requirements: Lane Tracing Assist (LTA) and Dynamic Radar Cruise Control (DRCC) + # US Market reference: "All", since all Corolla in the US come standard with LTA and DRCC + + # the simplest description of the requirements for the US market package: str + + # the minimum compatibility requirements for this model, regardless + # of market. can be a package, trim, or list of features + requirements: Optional[str] = None + video_link: Optional[str] = None footnotes: List[Enum] = field(default_factory=list) min_steer_speed: Optional[float] = None diff --git a/selfdrive/car/ecu_addrs.py b/selfdrive/car/ecu_addrs.py index e5d550fac8..868f12cdb8 100755 --- a/selfdrive/car/ecu_addrs.py +++ b/selfdrive/car/ecu_addrs.py @@ -9,6 +9,8 @@ from selfdrive.car import make_can_msg from selfdrive.boardd.boardd import can_list_to_can_capnp from system.swaglog import cloudlog +EcuAddrBusType = Tuple[int, Optional[int], int] + def make_tester_present_msg(addr, bus, subaddr=None): dat = [0x02, SERVICE_TYPE.TESTER_PRESENT, 0x0] @@ -33,16 +35,16 @@ def is_tester_present_response(msg: capnp.lib.capnp._DynamicStructReader, subadd return False -def get_all_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, bus: int, timeout: float = 1, debug: bool = True) -> Set[Tuple[int, Optional[int], int]]: +def get_all_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, bus: int, timeout: float = 1, debug: bool = True) -> Set[EcuAddrBusType]: addr_list = [0x700 + i for i in range(256)] + [0x18da00f1 + (i << 8) for i in range(256)] - queries: Set[Tuple[int, Optional[int], int]] = {(addr, None, bus) for addr in addr_list} + queries: Set[EcuAddrBusType] = {(addr, None, bus) for addr in addr_list} responses = queries return get_ecu_addrs(logcan, sendcan, queries, responses, timeout=timeout, debug=debug) -def get_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, queries: Set[Tuple[int, Optional[int], int]], - responses: Set[Tuple[int, Optional[int], int]], timeout: float = 1, debug: bool = False) -> Set[Tuple[int, Optional[int], int]]: - ecu_responses: Set[Tuple[int, Optional[int], int]] = set() # set((addr, subaddr, bus),) +def get_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, queries: Set[EcuAddrBusType], + responses: Set[EcuAddrBusType], timeout: float = 1, debug: bool = False) -> Set[EcuAddrBusType]: + ecu_responses: Set[EcuAddrBusType] = set() # set((addr, subaddr, bus),) try: msgs = [make_tester_present_msg(addr, bus, subaddr) for addr, subaddr, bus in queries] @@ -53,6 +55,10 @@ def get_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, que can_packets = messaging.drain_sock(logcan, wait_for_one=True) for packet in can_packets: for msg in packet.can: + if not len(msg.dat): + cloudlog.warning("ECU addr scan: skipping empty remote frame") + continue + subaddr = None if (msg.address, None, msg.src) in responses else msg.dat[0] if (msg.address, subaddr, msg.src) in responses and is_tester_present_response(msg, subaddr): if debug: diff --git a/selfdrive/car/ford/fordcan.py b/selfdrive/car/ford/fordcan.py index 594d50f59f..d2fb6fad55 100644 --- a/selfdrive/car/ford/fordcan.py +++ b/selfdrive/car/ford/fordcan.py @@ -158,8 +158,7 @@ def create_lkas_ui_msg(packer, main_on: bool, enabled: bool, steer_alert: bool, else: lines = 30 # LA_Off - # TODO: use level 1 for no sound when less severe? - hands_on_wheel_dsply = 2 if steer_alert else 0 + hands_on_wheel_dsply = 1 if steer_alert else 0 values = { **stock_values, diff --git a/selfdrive/car/ford/interface.py b/selfdrive/car/ford/interface.py index 9e1366618c..60571b42ef 100644 --- a/selfdrive/car/ford/interface.py +++ b/selfdrive/car/ford/interface.py @@ -11,7 +11,7 @@ GearShifter = car.CarState.GearShifter class CarInterface(CarInterfaceBase): @staticmethod - def _get_params(ret, candidate, fingerprint, car_fw, experimental_long): + def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs): ret.carName = "ford" ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.ford)] diff --git a/selfdrive/car/ford/values.py b/selfdrive/car/ford/values.py index 50c7d93987..c3652443c2 100644 --- a/selfdrive/car/ford/values.py +++ b/selfdrive/car/ford/values.py @@ -27,10 +27,11 @@ class CarControllerParams: STEER_DRIVER_ALLOWANCE = 1.0 # Driver intervention threshold, Nm # Curvature rate limits - # TODO: unify field names used by curvature and angle control cars - # ~2 m/s^3 up, ~-3 m/s^3 down - ANGLE_RATE_LIMIT_UP = AngleRateLimit(speed_bp=[5, 15, 25], angle_v=[0.004, 0.00044, 0.00016]) - ANGLE_RATE_LIMIT_DOWN = AngleRateLimit(speed_bp=[5, 15, 25], angle_v=[0.006, 0.00066, 0.00024]) + # The curvature signal is limited to 0.003 to 0.009 m^-1/sec by the EPS depending on speed and direction + # Limit to ~2 m/s^3 up, ~3 m/s^3 down at 75 mph + # Worst case, the low speed limits will allow 4.3 m/s^3 up, 4.9 m/s^3 down at 75 mph + ANGLE_RATE_LIMIT_UP = AngleRateLimit(speed_bp=[5, 25], angle_v=[0.0002, 0.0001]) + ANGLE_RATE_LIMIT_DOWN = AngleRateLimit(speed_bp=[5, 25], angle_v=[0.000225, 0.00015]) ACCEL_MAX = 2.0 # m/s^s max acceleration ACCEL_MIN = -3.5 # m/s^s max deceleration @@ -84,7 +85,7 @@ CAR_INFO: Dict[str, Union[CarInfo, List[CarInfo]]] = { FordCarInfo("Lincoln Aviator Plug-in Hybrid 2021", "Co-Pilot360 Plus"), ], CAR.FOCUS_MK4: FordCarInfo("Ford Focus EU 2019", "Driver Assistance Pack"), - CAR.MAVERICK_MK1: FordCarInfo("Ford Maverick 2022", "Co-Pilot360 Assist"), + CAR.MAVERICK_MK1: FordCarInfo("Ford Maverick 2022-23", "Co-Pilot360 Assist"), } FW_QUERY_CONFIG = FwQueryConfig( @@ -150,6 +151,7 @@ FW_VERSIONS = { b'LX6A-14C204-BJV\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'LX6A-14C204-ESG\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'MX6A-14C204-BEF\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'MX6A-14C204-BEJ\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'NX6A-14C204-BLE\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.shiftByWire, 0x732, None): [ @@ -217,6 +219,7 @@ FW_VERSIONS = { ], (Ecu.abs, 0x760, None): [ b'NZ6C-2D053-AG\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'PZ6C-2D053-ED\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x764, None): [ b'NZ6T-14D049-AA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', @@ -228,6 +231,7 @@ FW_VERSIONS = { b'NZ6A-14C204-AAA\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'NZ6A-14C204-PA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'NZ6A-14C204-ZA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'PZ6A-14C204-JC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.shiftByWire, 0x732, None): [ b'NZ6P-14G395-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', diff --git a/selfdrive/car/fw_query_definitions.py b/selfdrive/car/fw_query_definitions.py index dd3b19f6de..7ae9bee404 100755 --- a/selfdrive/car/fw_query_definitions.py +++ b/selfdrive/car/fw_query_definitions.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import capnp +import copy from dataclasses import dataclass, field import struct from typing import Dict, List, Optional, Tuple @@ -57,10 +58,12 @@ class Request: whitelist_ecus: List[int] = field(default_factory=list) rx_offset: int = 0x8 bus: int = 1 + # Whether this query should be run on the first auxiliary panda (CAN FD cars for example) + auxiliary: bool = False # FW responses from these queries will not be used for fingerprinting logging: bool = False - # These requests are done once OBD multiplexing is disabled, after all others - non_obd: bool = False + # boardd toggles OBD multiplexing on/off as needed + obd_multiplexing: bool = True @dataclass @@ -71,3 +74,10 @@ class FwQueryConfig: non_essential_ecus: Dict[capnp.lib.capnp._EnumModule, List[str]] = field(default_factory=dict) # Ecus added for data collection, not to be fingerprinted on extra_ecus: List[Tuple[capnp.lib.capnp._EnumModule, int, Optional[int]]] = field(default_factory=list) + + def __post_init__(self): + for i in range(len(self.requests)): + if self.requests[i].auxiliary: + new_request = copy.deepcopy(self.requests[i]) + new_request.bus += 4 + self.requests.append(new_request) diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index 8092ac0b76..1c0d5003ec 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 from collections import defaultdict -from typing import Any, Optional, Set, Tuple +from typing import Any, Dict, List, Set from tqdm import tqdm import panda.python.uds as uds from cereal import car from common.params import Params -from selfdrive.car.ecu_addrs import get_ecu_addrs +from selfdrive.car.ecu_addrs import EcuAddrBusType, get_ecu_addrs from selfdrive.car.interfaces import get_interface_attr from selfdrive.car.fingerprints import FW_VERSIONS from selfdrive.car.isotp_parallel_query import IsoTpParallelQuery @@ -19,7 +19,7 @@ FW_QUERY_CONFIGS = get_interface_attr('FW_QUERY_CONFIG', ignore_none=True) VERSIONS = get_interface_attr('FW_VERSIONS', ignore_none=True) MODEL_TO_BRAND = {c: b for b, e in VERSIONS.items() for c in e} -REQUESTS = [(brand, r) for brand, config in FW_QUERY_CONFIGS.items() for r in config.requests] +REQUESTS = [(brand, config, r) for brand, config in FW_QUERY_CONFIGS.items() for r in config.requests] def chunks(l, n=128): @@ -39,6 +39,8 @@ def build_fw_dict(fw_versions, filter_brand=None): def get_brand_addrs(): brand_addrs = defaultdict(set) for brand, cars in VERSIONS.items(): + # Add ecus in database + extra ecus to match against + brand_addrs[brand] |= {(addr, sub_addr) for _, addr, sub_addr in FW_QUERY_CONFIGS[brand].extra_ecus} for fw in cars.values(): brand_addrs[brand] |= {(addr, sub_addr) for _, addr, sub_addr in fw.keys()} return brand_addrs @@ -146,38 +148,43 @@ def match_fw_to_car(fw_versions, allow_exact=True, allow_fuzzy=True): return True, set() -def get_present_ecus(logcan, sendcan, num_pandas=1) -> Set[Tuple[int, Optional[int], int]]: - queries = list() - parallel_queries = list() +def get_present_ecus(logcan, sendcan, num_pandas=1) -> Set[EcuAddrBusType]: + params = Params() + # queries are split by OBD multiplexing mode + queries: Dict[bool, List[List[EcuAddrBusType]]] = {True: [], False: []} + parallel_queries: Dict[bool, List[EcuAddrBusType]] = {True: [], False: []} responses = set() - for brand, r in REQUESTS: + for brand, config, r in REQUESTS: # Skip query if no panda available if r.bus > num_pandas * 4 - 1: continue for brand_versions in VERSIONS[brand].values(): - for ecu_type, addr, sub_addr in brand_versions: + for ecu_type, addr, sub_addr in list(brand_versions) + config.extra_ecus: # Only query ecus in whitelist if whitelist is not empty if len(r.whitelist_ecus) == 0 or ecu_type in r.whitelist_ecus: a = (addr, sub_addr, r.bus) # Build set of queries if sub_addr is None: - if a not in parallel_queries: - parallel_queries.append(a) + if a not in parallel_queries[r.obd_multiplexing]: + parallel_queries[r.obd_multiplexing].append(a) else: # subaddresses must be queried one by one - if [a] not in queries: - queries.append([a]) + if [a] not in queries[r.obd_multiplexing]: + queries[r.obd_multiplexing].append([a]) # Build set of expected responses to filter response_addr = uds.get_rx_addr_for_tx_addr(addr, r.rx_offset) responses.add((response_addr, sub_addr, r.bus)) - queries.insert(0, parallel_queries) + for obd_multiplexing in queries: + queries[obd_multiplexing].insert(0, parallel_queries[obd_multiplexing]) ecu_responses = set() - for query in queries: - ecu_responses.update(get_ecu_addrs(logcan, sendcan, set(query), responses, timeout=0.1)) + for obd_multiplexing in queries: + set_obd_multiplexing(params, obd_multiplexing) + for query in queries[obd_multiplexing]: + ecu_responses.update(get_ecu_addrs(logcan, sendcan, set(query), responses, timeout=0.1)) return ecu_responses @@ -185,9 +192,9 @@ def get_brand_ecu_matches(ecu_rx_addrs): """Returns dictionary of brands and matches with ECUs in their FW versions""" brand_addrs = get_brand_addrs() - brand_matches = {brand: set() for brand, _ in REQUESTS} + brand_matches = {brand: set() for brand, _, _ in REQUESTS} - brand_rx_offsets = set((brand, r.rx_offset) for brand, r in REQUESTS) + brand_rx_offsets = set((brand, r.rx_offset) for brand, _, r in REQUESTS) for addr, sub_addr, _ in ecu_rx_addrs: # Since we can't know what request an ecu responded to, add matches for all possible rx offsets for brand, rx_offset in brand_rx_offsets: @@ -198,13 +205,13 @@ def get_brand_ecu_matches(ecu_rx_addrs): return brand_matches -def disable_obd_multiplexing(params): - if not params.get_bool("ObdMultiplexingDisabled"): - params.put_bool("FirmwareObdQueryDone", True) - - cloudlog.warning("Waiting for OBD multiplexing to be disabled") - params.get_bool("ObdMultiplexingDisabled", block=True) - cloudlog.warning("OBD multiplexing disabled") +def set_obd_multiplexing(params: Params, obd_multiplexing: bool): + if params.get_bool("ObdMultiplexingEnabled") != obd_multiplexing: + cloudlog.warning(f"Setting OBD multiplexing to {obd_multiplexing}") + params.remove("ObdMultiplexingChanged") + params.put_bool("ObdMultiplexingEnabled", obd_multiplexing) + params.get_bool("ObdMultiplexingChanged", block=True) + cloudlog.warning("OBD multiplexing set successfully") def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, num_pandas=1, debug=False, progress=False): @@ -212,29 +219,25 @@ def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, num_pand all_car_fw = [] brand_matches = get_brand_ecu_matches(ecu_rx_addrs) - matched_brand: Optional[str] = None for brand in sorted(brand_matches, key=lambda b: len(brand_matches[b]), reverse=True): + # Skip this brand if there are no matching present ECUs + if not len(brand_matches[brand]): + continue + car_fw = get_fw_versions(logcan, sendcan, query_brand=brand, timeout=timeout, num_pandas=num_pandas, debug=debug, progress=progress) all_car_fw.extend(car_fw) # Try to match using FW returned from this brand only matches = match_fw_to_car_exact(build_fw_dict(car_fw)) if len(matches) == 1: - matched_brand = brand break - disable_obd_multiplexing(Params()) - - # Do non-OBD queries for matched brand, or all if no match is found - for brand in FW_QUERY_CONFIGS.keys(): - if brand == matched_brand or matched_brand is None: - all_car_fw.extend(get_fw_versions(logcan, sendcan, query_brand=brand, timeout=timeout, num_pandas=num_pandas, obd_multiplexed=False, debug=debug, progress=progress)) - return all_car_fw -def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, num_pandas=1, obd_multiplexed=True, debug=False, progress=False): +def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, num_pandas=1, debug=False, progress=False): versions = VERSIONS.copy() + params = Params() # Each brand can define extra ECUs to query for data collection for brand, config in FW_QUERY_CONFIGS.items(): @@ -250,19 +253,15 @@ def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, # ECUs using a subaddress need be queried one by one, the rest can be done in parallel addrs = [] parallel_addrs = [] - logging_addrs = [] ecu_types = {} for brand, brand_versions in versions.items(): - for candidate, ecu in brand_versions.items(): + for ecu in brand_versions.values(): for ecu_type, addr, sub_addr in ecu.keys(): a = (brand, addr, sub_addr) if a not in ecu_types: ecu_types[a] = ecu_type - if a not in logging_addrs and candidate == "debug": - logging_addrs.append(a) - if sub_addr is None: if a not in parallel_addrs: parallel_addrs.append(a) @@ -274,16 +273,17 @@ def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, # Get versions and build capnp list to put into CarParams car_fw = [] - requests = [(brand, r) for brand, r in REQUESTS if query_brand is None or brand == query_brand] + requests = [(brand, config, r) for brand, config, r in REQUESTS if query_brand is None or brand == query_brand] for addr in tqdm(addrs, disable=not progress): for addr_chunk in chunks(addr): - for brand, r in requests: + for brand, config, r in requests: # Skip query if no panda available if r.bus > num_pandas * 4 - 1: continue - # Or if request is not designated for current multiplexing mode - elif r.non_obd == obd_multiplexed: - continue + + # Toggle OBD multiplexing for each request + if r.bus % 4 == 1: + set_obd_multiplexing(params, r.obd_multiplexing) try: addrs = [(a, s) for (b, a, s) in addr_chunk if b in (brand, 'any') and @@ -294,15 +294,15 @@ def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, for (tx_addr, sub_addr), version in query.get_data(timeout).items(): f = car.CarParams.CarFw.new_message() - ecu_key = (brand, tx_addr, sub_addr) - f.ecu = ecu_types.get(ecu_key, Ecu.unknown) + f.ecu = ecu_types.get((brand, tx_addr, sub_addr), Ecu.unknown) f.fwVersion = version f.address = tx_addr f.responseAddress = uds.get_rx_addr_for_tx_addr(tx_addr, r.rx_offset) f.request = r.request f.brand = brand f.bus = r.bus - f.logging = r.logging or ecu_key in logging_addrs + f.logging = r.logging or (f.ecu, tx_addr, sub_addr) in config.extra_ecus + f.obdMultiplexing = r.obd_multiplexing if sub_addr is not None: f.subAddress = sub_addr diff --git a/selfdrive/car/gm/interface.py b/selfdrive/car/gm/interface.py index 602d7e31b6..ff578da986 100755 --- a/selfdrive/car/gm/interface.py +++ b/selfdrive/car/gm/interface.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 from cereal import car -from math import fabs +from math import fabs, exp from panda import Panda from common.conversions import Conversions as CV from selfdrive.car import STD_CARGO_KG, create_button_event, scale_tire_stiffness, get_safety_config from selfdrive.car.gm.radar_interface import RADAR_HEADER_MSG from selfdrive.car.gm.values import CAR, CruiseButtons, CarControllerParams, EV_CAR, CAMERA_ACC_CAR, CanBus -from selfdrive.car.interfaces import CarInterfaceBase +from selfdrive.car.interfaces import CarInterfaceBase, TorqueFromLateralAccelCallbackType, FRICTION_THRESHOLD +from selfdrive.controls.lib.drive_helpers import get_friction ButtonType = car.CarState.ButtonEvent.Type EventName = car.CarEvent.EventName @@ -45,11 +46,33 @@ class CarInterface(CarInterfaceBase): return CarInterfaceBase.get_steer_feedforward_default @staticmethod - def _get_params(ret, candidate, fingerprint, car_fw, experimental_long): + def torque_from_lateral_accel_bolt(lateral_accel_value: float, torque_params: car.CarParams.LateralTorqueTuning, + lateral_accel_error: float, lateral_accel_deadzone: float, friction_compensation: bool) -> float: + friction = get_friction(lateral_accel_error, lateral_accel_deadzone, FRICTION_THRESHOLD, torque_params, friction_compensation) + + def sig(val): + return 1 / (1 + exp(-val)) - 0.5 + + # The "lat_accel vs torque" relationship is assumed to be the sum of "sigmoid + linear" curves + # An important thing to consider is that the slope at 0 should be > 0 (ideally >1) + # This has big effect on the stability about 0 (noise when going straight) + # ToDo: To generalize to other GMs, explore tanh function as the nonlinear + a, b, c, _ = [2.6531724862969748, 1.0, 0.1919764879840985, 0.009054123646805178] # weights computed offline + + steer_torque = (sig(lateral_accel_value * a) * b) + (lateral_accel_value * c) + return float(steer_torque) + friction + + def torque_from_lateral_accel(self) -> TorqueFromLateralAccelCallbackType: + if self.CP.carFingerprint == CAR.BOLT_EUV: + return self.torque_from_lateral_accel_bolt + else: + return self.torque_from_lateral_accel_linear + + @staticmethod + def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs): ret.carName = "gm" ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.gm)] ret.autoResumeSng = False - use_off_car_defaults = len(fingerprint[0]) == 0 # Pick sensible carParams during offline doc generation/CI jobs if candidate in EV_CAR: ret.transmissionType = TransmissionType.direct @@ -74,11 +97,9 @@ class CarInterface(CarInterfaceBase): # Tuning for experimental long ret.longitudinalTuning.kpV = [2.0, 1.5] ret.longitudinalTuning.kiV = [0.72] - ret.stopAccel = -2.0 ret.stoppingDecelRate = 2.0 # reach brake quickly after enabling ret.vEgoStopping = 0.25 ret.vEgoStarting = 0.25 - ret.longitudinalActuatorDelayUpperBound = 0.5 if experimental_long: ret.pcmCruise = False @@ -88,7 +109,7 @@ class CarInterface(CarInterfaceBase): else: # ASCM, OBD-II harness ret.openpilotLongitudinalControl = True ret.networkLocation = NetworkLocation.gateway - ret.radarUnavailable = RADAR_HEADER_MSG not in fingerprint[CanBus.OBSTACLE] and not use_off_car_defaults + ret.radarUnavailable = RADAR_HEADER_MSG not in fingerprint[CanBus.OBSTACLE] and not docs ret.pcmCruise = False # stock non-adaptive cruise control is kept off # supports stop and go, but initial engage must (conservatively) be above 18mph ret.minEnableSpeed = 18 * CV.MPH_TO_MS @@ -113,6 +134,7 @@ class CarInterface(CarInterfaceBase): ret.steerLimitTimer = 0.4 ret.radarTimeStep = 0.0667 # GM radar runs at 15Hz instead of standard 20Hz + ret.longitudinalActuatorDelayUpperBound = 0.5 # large delay to initially start braking if candidate == CAR.VOLT: ret.mass = 1607. + STD_CARGO_KG @@ -148,7 +170,6 @@ class CarInterface(CarInterfaceBase): ret.steerRatio = 14.4 # end to end is 13.46 ret.centerToFront = ret.wheelbase * 0.4 ret.lateralTuning.pid.kf = 1. # get_steer_feedforward_acadia() - ret.longitudinalActuatorDelayUpperBound = 0.5 # large delay to initially start braking elif candidate == CAR.BUICK_LACROSSE: ret.mass = 1712. + STD_CARGO_KG @@ -175,7 +196,6 @@ class CarInterface(CarInterfaceBase): ret.wheelbase = 2.95 # 116 inches in meters ret.steerRatio = 17.3 ret.centerToFront = ret.wheelbase * 0.5 - ret.longitudinalActuatorDelayUpperBound = 0.5 # large delay to initially start braking CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) elif candidate == CAR.ESCALADE_ESV: @@ -213,6 +233,15 @@ class CarInterface(CarInterfaceBase): ret.centerToFront = ret.wheelbase * 0.4 CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) + elif candidate == CAR.TRAILBLAZER: + ret.mass = 1345. + STD_CARGO_KG + ret.wheelbase = 2.64 + ret.steerRatio = 16.8 + ret.centerToFront = ret.wheelbase * 0.4 + tire_stiffness_factor = 1.0 + ret.steerActuatorDelay = 0.2 + CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) + # TODO: start from empirically derived lateral slip stiffness for the civic and scale by # mass and CG position, so all cars will have approximately similar dyn behaviors ret.tireStiffnessFront, ret.tireStiffnessRear = scale_tire_stiffness(ret.mass, ret.wheelbase, ret.centerToFront, diff --git a/selfdrive/car/gm/values.py b/selfdrive/car/gm/values.py index 0d13cb9827..bc2858a667 100644 --- a/selfdrive/car/gm/values.py +++ b/selfdrive/car/gm/values.py @@ -14,8 +14,8 @@ class CarControllerParams: STEER_STEP = 3 # Active control frames per command (~33hz) INACTIVE_STEER_STEP = 10 # Inactive control frames per command (10hz) STEER_DELTA_UP = 10 # Delta rates require review due to observed EPS weakness - STEER_DELTA_DOWN = 25 - STEER_DRIVER_ALLOWANCE = 50 + STEER_DELTA_DOWN = 15 + STEER_DRIVER_ALLOWANCE = 65 STEER_DRIVER_MULTIPLIER = 4 STEER_DRIVER_FACTOR = 100 NEAR_STOP_BRAKE_PHASE = 0.5 # m/s @@ -73,6 +73,7 @@ class CAR: BOLT_EUV = "CHEVROLET BOLT EUV 2022" SILVERADO = "CHEVROLET SILVERADO 1500 2020" EQUINOX = "CHEVROLET EQUINOX 2019" + TRAILBLAZER = "CHEVROLET TRAILBLAZER 2021" class Footnote(Enum): @@ -105,14 +106,15 @@ CAR_INFO: Dict[str, Union[GMCarInfo, List[GMCarInfo]]] = { CAR.ESCALADE: GMCarInfo("Cadillac Escalade 2017", "Driver Assist Package"), CAR.ESCALADE_ESV: GMCarInfo("Cadillac Escalade ESV 2016", "Adaptive Cruise Control (ACC) & LKAS"), CAR.BOLT_EUV: [ - GMCarInfo("Chevrolet Bolt EUV 2022-23", "Premier or Premier Redline Trim without Super Cruise Package", "https://youtu.be/xvwzGMUA210"), + GMCarInfo("Chevrolet Bolt EUV 2022-23", "Premier or Premier Redline Trim without Super Cruise Package", video_link="https://youtu.be/xvwzGMUA210"), GMCarInfo("Chevrolet Bolt EV 2022-23", "2LT Trim with Adaptive Cruise Control Package"), ], CAR.SILVERADO: [ GMCarInfo("Chevrolet Silverado 1500 2020-21", "Safety Package II"), - GMCarInfo("GMC Sierra 1500 2020-21", "Driver Alert Package II", "https://youtu.be/5HbNoBLzRwE"), + GMCarInfo("GMC Sierra 1500 2020-21", "Driver Alert Package II", video_link="https://youtu.be/5HbNoBLzRwE"), ], CAR.EQUINOX: GMCarInfo("Chevrolet Equinox 2019-22"), + CAR.TRAILBLAZER: GMCarInfo("Chevrolet Trailblazer 2021-22"), } @@ -207,6 +209,12 @@ FINGERPRINTS = { { 190: 6, 193: 8, 197: 8, 201: 8, 209: 7, 211: 2, 241: 6, 249: 8, 257: 8, 288: 5, 289: 8, 298: 8, 304: 1, 309: 8, 311: 8, 313: 8, 320: 3, 328: 1, 352: 5, 381: 8, 384: 4, 386: 8, 388: 8, 413: 8, 451: 8, 452: 8, 453: 6, 455: 7, 463: 3, 479: 3, 481: 7, 485: 8, 489: 8, 497: 8, 500: 6, 501: 8, 510: 8, 528: 5, 532: 6, 560: 8, 562: 8, 563: 5, 565: 5, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 707: 8, 715: 8, 717: 5, 753: 5, 761: 7, 789: 5, 800: 6, 810: 8, 840: 5, 842: 5, 844: 8, 869: 4, 880: 6, 977: 8, 1001: 8, 1011: 6, 1017: 8, 1020: 8, 1033: 7, 1034: 7, 1217: 8, 1221: 5, 1233: 8, 1249: 8, 1259: 8, 1261: 7, 1263: 4, 1265: 8, 1267: 1, 1271: 8, 1280: 4, 1296: 4, 1300: 8, 1930: 7 }], + # Trailblazer also matches as a Silverado, so comment out to avoid conflicts. + # TODO: split with FW versions + CAR.TRAILBLAZER: [ + { + # 190: 6, 193: 8, 197: 8, 201: 8, 209: 7, 211: 2, 241: 6, 249: 8, 288: 5, 289: 8, 298: 8, 304: 3, 309: 8, 311: 8, 313: 8, 320: 4, 328: 1, 352: 5, 381: 8, 384: 4, 386: 8, 388: 8, 413: 8, 451: 8, 452: 8, 453: 6, 455: 7, 479: 3, 481: 7, 485: 8, 489: 8, 497: 8, 500: 6, 501: 8, 532: 6, 560: 8, 562: 8, 563: 5, 565: 5, 707: 8, 715: 8, 717: 5, 761: 7, 789: 5, 800: 6, 810: 8, 840: 5, 842: 5, 844: 8, 869: 4, 880: 6, 977: 8, 1001: 8, 1011: 6, 1017: 8, 1020: 8, 1217: 8, 1221: 5, 1233: 8, 1249: 8, 1259: 8, 1261: 7, 1263: 4, 1265: 8, 1267: 1, 1271: 8, 1280: 4, 1296: 4, 1300: 8, 1609: 8, 1613: 8, 1649: 8, 1792: 8, 1798: 8, 1824: 8, 1825: 8, 1840: 8, 1842: 8, 1858: 8, 1860: 8, 1863: 8, 1872: 8, 1875: 8, 1882: 8, 1888: 8, 1889: 8, 1892: 8, 1930: 7, 1937: 8, 1953: 8, 1968: 8, 2001: 8, 2017: 8, 2018: 8, 2020: 8 + }], } DBC: Dict[str, Dict[str, str]] = defaultdict(lambda: dbc_dict('gm_global_a_powertrain_generated', 'gm_global_a_object', chassis_dbc='gm_global_a_chassis')) @@ -214,6 +222,6 @@ DBC: Dict[str, Dict[str, str]] = defaultdict(lambda: dbc_dict('gm_global_a_power EV_CAR = {CAR.VOLT, CAR.BOLT_EUV} # We're integrated at the camera with VOACC on these cars (instead of ASCM w/ OBD-II harness) -CAMERA_ACC_CAR = {CAR.BOLT_EUV, CAR.SILVERADO, CAR.EQUINOX} +CAMERA_ACC_CAR = {CAR.BOLT_EUV, CAR.SILVERADO, CAR.EQUINOX, CAR.TRAILBLAZER} STEER_THRESHOLD = 1.0 diff --git a/selfdrive/car/honda/carcontroller.py b/selfdrive/car/honda/carcontroller.py index 4dc1dc8131..347c16c86f 100644 --- a/selfdrive/car/honda/carcontroller.py +++ b/selfdrive/car/honda/carcontroller.py @@ -1,7 +1,6 @@ from collections import namedtuple from cereal import car -from common.conversions import Conversions as CV from common.numpy_fast import clip, interp from common.realtime import DT_CTRL from opendbc.can.packer import CANPacker @@ -117,6 +116,7 @@ class CarController: self.brake_last = 0. self.apply_brake_last = 0 self.last_pump_ts = 0. + self.stopping_counter = 0 self.accel = 0.0 self.speed = 0.0 @@ -127,7 +127,8 @@ class CarController: def update(self, CC, CS, now_nanos): actuators = CC.actuators hud_control = CC.hudControl - hud_v_cruise = hud_control.setSpeed * CV.MS_TO_KPH if hud_control.speedVisible else 255 + conversion = hondacan.get_cruise_speed_conversion(self.CP.carFingerprint, CS.is_metric) + hud_v_cruise = hud_control.setSpeed / conversion if hud_control.speedVisible else 255 pcm_cancel_cmd = CC.cruiseControl.cancel if CC.longActive: @@ -161,7 +162,7 @@ class CarController: can_sends = [] # tester present - w/ no response (keeps radar disabled) - if self.CP.carFingerprint in HONDA_BOSCH and self.CP.openpilotLongitudinalControl: + if self.CP.carFingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS) and self.CP.openpilotLongitudinalControl: if self.frame % 10 == 0: can_sends.append((0x18DAB0F1, 0, b"\x02\x3E\x80\x00\x00\x00\x00\x00", 1)) @@ -217,8 +218,9 @@ class CarController: self.gas = interp(accel, self.params.BOSCH_GAS_LOOKUP_BP, self.params.BOSCH_GAS_LOOKUP_V) stopping = actuators.longControlState == LongCtrlState.stopping + self.stopping_counter = self.stopping_counter + 1 if stopping else 0 can_sends.extend(hondacan.create_acc_commands(self.packer, CC.enabled, CC.longActive, self.accel, self.gas, - stopping, self.CP.carFingerprint)) + self.stopping_counter, self.CP.carFingerprint)) else: apply_brake = clip(self.brake_last - wind_brake, 0.0, 1.0) apply_brake = int(clip(apply_brake * self.params.NIDEC_BRAKE_MAX, 0, self.params.NIDEC_BRAKE_MAX - 1)) diff --git a/selfdrive/car/honda/carstate.py b/selfdrive/car/honda/carstate.py index 16880d1b1f..bcc239c2df 100644 --- a/selfdrive/car/honda/carstate.py +++ b/selfdrive/car/honda/carstate.py @@ -5,7 +5,7 @@ from common.conversions import Conversions as CV from common.numpy_fast import interp from opendbc.can.can_define import CANDefine from opendbc.can.parser import CANParser -from selfdrive.car.honda.hondacan import get_pt_bus +from selfdrive.car.honda.hondacan import get_cruise_speed_conversion, get_pt_bus from selfdrive.car.honda.values import CAR, DBC, STEER_THRESHOLD, HONDA_BOSCH, HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_ALT_BRAKE_SIGNAL, HONDA_BOSCH_RADARLESS from selfdrive.car.interfaces import CarStateBase @@ -103,7 +103,7 @@ def get_can_signals(CP, gearbox_msg, main_on_sig_msg): else: checks.append(("CRUISE_PARAMS", 50)) - if CP.carFingerprint in (CAR.ACCORD, CAR.ACCORDH, CAR.CIVIC_BOSCH, CAR.CIVIC_BOSCH_DIESEL, CAR.CRV_HYBRID, CAR.INSIGHT, CAR.ACURA_RDX_3G, CAR.HONDA_E, CAR.CIVIC_2022): + if CP.carFingerprint in (CAR.ACCORD, CAR.ACCORDH, CAR.CIVIC_BOSCH, CAR.CIVIC_BOSCH_DIESEL, CAR.CRV_HYBRID, CAR.INSIGHT, CAR.ACURA_RDX_3G, CAR.HONDA_E, CAR.CIVIC_2022, CAR.HRV_3G): signals.append(("DRIVERS_DOOR_OPEN", "SCM_FEEDBACK")) elif CP.carFingerprint in (CAR.ODYSSEY_CHN, CAR.FREED, CAR.HRV): signals.append(("DRIVERS_DOOR_OPEN", "SCM_BUTTONS")) @@ -120,7 +120,10 @@ def get_can_signals(CP, gearbox_msg, main_on_sig_msg): signals.append(("INTERCEPTOR_GAS2", "GAS_SENSOR")) checks.append(("GAS_SENSOR", 50)) - if CP.openpilotLongitudinalControl: + if CP.carFingerprint in HONDA_BOSCH_RADARLESS: + signals.append(("CRUISE_FAULT", "CRUISE_FAULT_STATUS")) + checks.append(("CRUISE_FAULT_STATUS", 50)) + elif CP.openpilotLongitudinalControl: signals += [ ("BRAKE_ERROR_1", "STANDSTILL"), ("BRAKE_ERROR_2", "STANDSTILL") @@ -176,7 +179,7 @@ class CarState(CarStateBase): # panda checks if the signal is non-zero ret.standstill = cp.vl["ENGINE_DATA"]["XMISSION_SPEED"] < 1e-5 # TODO: find a common signal across all cars - if self.CP.carFingerprint in (CAR.ACCORD, CAR.ACCORDH, CAR.CIVIC_BOSCH, CAR.CIVIC_BOSCH_DIESEL, CAR.CRV_HYBRID, CAR.INSIGHT, CAR.ACURA_RDX_3G, CAR.HONDA_E, CAR.CIVIC_2022): + if self.CP.carFingerprint in (CAR.ACCORD, CAR.ACCORDH, CAR.CIVIC_BOSCH, CAR.CIVIC_BOSCH_DIESEL, CAR.CRV_HYBRID, CAR.INSIGHT, CAR.ACURA_RDX_3G, CAR.HONDA_E, CAR.CIVIC_2022, CAR.HRV_3G): ret.doorOpen = bool(cp.vl["SCM_FEEDBACK"]["DRIVERS_DOOR_OPEN"]) elif self.CP.carFingerprint in (CAR.ODYSSEY_CHN, CAR.FREED, CAR.HRV): ret.doorOpen = bool(cp.vl["SCM_BUTTONS"]["DRIVERS_DOOR_OPEN"]) @@ -191,7 +194,9 @@ class CarState(CarStateBase): # NO_TORQUE_ALERT_2 can be caused by bump or steering nudge from driver ret.steerFaultTemporary = steer_status not in ("NORMAL", "LOW_SPEED_LOCKOUT", "NO_TORQUE_ALERT_2") - if self.CP.openpilotLongitudinalControl: + if self.CP.carFingerprint in HONDA_BOSCH_RADARLESS: + self.brake_error = cp.vl["CRUISE_FAULT_STATUS"]["CRUISE_FAULT"] + elif self.CP.openpilotLongitudinalControl: self.brake_error = cp.vl["STANDSTILL"]["BRAKE_ERROR_1"] or cp.vl["STANDSTILL"]["BRAKE_ERROR_2"] ret.espDisabled = cp.vl["VSA_STATUS"]["ESP_DISABLED"] != 0 @@ -246,8 +251,7 @@ class CarState(CarStateBase): ret.cruiseState.nonAdaptive = acc_hud["CRUISE_CONTROL_LABEL"] != 0 ret.cruiseState.standstill = acc_hud["CRUISE_SPEED"] == 252. - # on certain cars, CRUISE_SPEED changes to imperial with car's unit setting - conversion = CV.MPH_TO_MS if self.CP.carFingerprint in HONDA_BOSCH_RADARLESS and not self.is_metric else CV.KPH_TO_MS + conversion = get_cruise_speed_conversion(self.CP.carFingerprint, self.is_metric) # On set, cruise set speed pulses between 254~255 and the set speed prev is set to avoid this. ret.cruiseState.speed = self.v_cruise_pcm_prev if acc_hud["CRUISE_SPEED"] > 160.0 else acc_hud["CRUISE_SPEED"] * conversion self.v_cruise_pcm_prev = ret.cruiseState.speed @@ -294,7 +298,7 @@ class CarState(CarStateBase): if self.CP.carFingerprint in HONDA_BOSCH_RADARLESS: self.lkas_hud = cp_cam.vl["LKAS_HUD"] - if self.CP.enableBsm and self.CP.carFingerprint in (CAR.CRV_5G, ): + if self.CP.enableBsm: # BSM messages are on B-CAN, requires a panda forwarding B-CAN messages to CAN 0 # more info here: https://github.com/commaai/openpilot/pull/1867 ret.leftBlindspot = cp_body.vl["BSM_STATUS_LEFT"]["BSM_ALERT"] == 1 @@ -341,7 +345,7 @@ class CarState(CarStateBase): @staticmethod def get_body_can_parser(CP): - if CP.enableBsm and CP.carFingerprint == CAR.CRV_5G: + if CP.enableBsm: signals = [("BSM_ALERT", "BSM_STATUS_RIGHT"), ("BSM_ALERT", "BSM_STATUS_LEFT")] diff --git a/selfdrive/car/honda/hondacan.py b/selfdrive/car/honda/hondacan.py index 17681444af..1fe0a13767 100644 --- a/selfdrive/car/honda/hondacan.py +++ b/selfdrive/car/honda/hondacan.py @@ -21,6 +21,11 @@ def get_lkas_cmd_bus(car_fingerprint, radar_disabled=False): return 0 +def get_cruise_speed_conversion(car_fingerprint: str, is_metric: bool) -> float: + # on certain cars, CRUISE_SPEED changes to imperial with car's unit setting + return CV.MPH_TO_MS if car_fingerprint in HONDA_BOSCH_RADARLESS and not is_metric else CV.KPH_TO_MS + + def create_brake_command(packer, apply_brake, pump_on, pcm_override, pcm_cancel_cmd, fcw, car_fingerprint, stock_brake): # TODO: do we loose pressure if we keep pump off for long? brakelights = apply_brake > 0 @@ -46,7 +51,7 @@ def create_brake_command(packer, apply_brake, pump_on, pcm_override, pcm_cancel_ return packer.make_can_msg("BRAKE_COMMAND", bus, values) -def create_acc_commands(packer, enabled, active, accel, gas, stopping, car_fingerprint): +def create_acc_commands(packer, enabled, active, accel, gas, stopping_counter, car_fingerprint): commands = [] bus = get_pt_bus(car_fingerprint) min_gas_accel = CarControllerParams.BOSCH_GAS_LOOKUP_BP[0] @@ -55,30 +60,39 @@ def create_acc_commands(packer, enabled, active, accel, gas, stopping, car_finge gas_command = gas if active and accel > min_gas_accel else -30000 accel_command = accel if active else 0 braking = 1 if active and accel < min_gas_accel else 0 - standstill = 1 if active and stopping else 0 - standstill_release = 1 if active and not stopping else 0 + standstill = 1 if active and stopping_counter > 0 else 0 + standstill_release = 1 if active and stopping_counter == 0 else 0 + # common ACC_CONTROL values acc_control_values = { - # setting CONTROL_ON causes car to set POWERTRAIN_DATA->ACC_STATUS = 1 - "CONTROL_ON": control_on, - "GAS_COMMAND": gas_command, # used for gas - "ACCEL_COMMAND": accel_command, # used for brakes - "BRAKE_LIGHTS": braking, - "BRAKE_REQUEST": braking, - "STANDSTILL": standstill, - "STANDSTILL_RELEASE": standstill_release, + 'ACCEL_COMMAND': accel_command, + 'STANDSTILL': standstill, } - commands.append(packer.make_can_msg("ACC_CONTROL", bus, acc_control_values)) - acc_control_on_values = { - "SET_TO_3": 0x03, - "CONTROL_ON": enabled, - "SET_TO_FF": 0xff, - "SET_TO_75": 0x75, - "SET_TO_30": 0x30, - } - commands.append(packer.make_can_msg("ACC_CONTROL_ON", bus, acc_control_on_values)) + if car_fingerprint in HONDA_BOSCH_RADARLESS: + acc_control_values.update({ + "CONTROL_ON": enabled, + "IDLESTOP_ALLOW": stopping_counter > 200, # allow idle stop after 4 seconds (50 Hz) + }) + else: + acc_control_values.update({ + # setting CONTROL_ON causes car to set POWERTRAIN_DATA->ACC_STATUS = 1 + "CONTROL_ON": control_on, + "GAS_COMMAND": gas_command, # used for gas + "BRAKE_LIGHTS": braking, + "BRAKE_REQUEST": braking, + "STANDSTILL_RELEASE": standstill_release, + }) + acc_control_on_values = { + "SET_TO_3": 0x03, + "CONTROL_ON": enabled, + "SET_TO_FF": 0xff, + "SET_TO_75": 0x75, + "SET_TO_30": 0x30, + } + commands.append(packer.make_can_msg("ACC_CONTROL_ON", bus, acc_control_on_values)) + commands.append(packer.make_can_msg("ACC_CONTROL", bus, acc_control_values)) return commands diff --git a/selfdrive/car/honda/interface.py b/selfdrive/car/honda/interface.py index d3cf9fa891..65deab7401 100755 --- a/selfdrive/car/honda/interface.py +++ b/selfdrive/car/honda/interface.py @@ -31,19 +31,17 @@ class CarInterface(CarInterfaceBase): return CarControllerParams.NIDEC_ACCEL_MIN, interp(current_speed, ACCEL_MAX_BP, ACCEL_MAX_VALS) @staticmethod - def _get_params(ret, candidate, fingerprint, car_fw, experimental_long): + def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs): ret.carName = "honda" if candidate in HONDA_BOSCH: ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.hondaBosch)] ret.radarUnavailable = True - - if candidate not in HONDA_BOSCH_RADARLESS: - # Disable the radar and let openpilot control longitudinal - # WARNING: THIS DISABLES AEB! - ret.experimentalLongitudinalAvailable = True - ret.openpilotLongitudinalControl = experimental_long - + # Disable the radar and let openpilot control longitudinal + # WARNING: THIS DISABLES AEB! + # If Bosch radarless, this blocks ACC messages from the camera + ret.experimentalLongitudinalAvailable = True + ret.openpilotLongitudinalControl = experimental_long ret.pcmCruise = not ret.openpilotLongitudinalControl else: ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.hondaNidec)] @@ -75,6 +73,8 @@ class CarInterface(CarInterfaceBase): ret.longitudinalTuning.kpV = [0.25] ret.longitudinalTuning.kiV = [0.05] ret.longitudinalActuatorDelayUpperBound = 0.5 # s + if candidate in HONDA_BOSCH_RADARLESS: + ret.stopAccel = CarControllerParams.BOSCH_ACCEL_MIN # stock uses -4.0 m/s^2 once stopped but limited by safety model else: # default longitudinal tuning for all hondas ret.longitudinalTuning.kpBP = [0., 5., 35.] @@ -193,15 +193,18 @@ class CarInterface(CarInterfaceBase): tire_stiffness_factor = 0.75 ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.05]] - elif candidate == CAR.HRV: + elif candidate in (CAR.HRV, CAR.HRV_3G): ret.mass = 3125 * CV.LB_TO_KG + STD_CARGO_KG ret.wheelbase = 2.61 ret.centerToFront = ret.wheelbase * 0.41 ret.steerRatio = 15.2 ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] tire_stiffness_factor = 0.5 - ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.16], [0.025]] - ret.wheelSpeedFactor = 1.025 + if candidate == CAR.HRV: + ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.16], [0.025]] + ret.wheelSpeedFactor = 1.025 + else: + ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.8], [0.24]] # TODO: can probably use some tuning elif candidate == CAR.ACURA_RDX: ret.mass = 3935. * CV.LB_TO_KG + STD_CARGO_KG diff --git a/selfdrive/car/honda/values.py b/selfdrive/car/honda/values.py index 0737bea147..951d3c8218 100644 --- a/selfdrive/car/honda/values.py +++ b/selfdrive/car/honda/values.py @@ -87,6 +87,7 @@ class CAR: FIT = "HONDA FIT 2018" FREED = "HONDA FREED 2020" HRV = "HONDA HRV 2019" + HRV_3G = "HONDA HR-V 2023" ODYSSEY = "HONDA ODYSSEY 2018" ODYSSEY_CHN = "HONDA ODYSSEY CHN 2019" ACURA_RDX = "ACURA RDX 2018" @@ -116,19 +117,19 @@ class HondaCarInfo(CarInfo): CAR_INFO: Dict[str, Optional[Union[HondaCarInfo, List[HondaCarInfo]]]] = { CAR.ACCORD: [ - HondaCarInfo("Honda Accord 2018-22", "All", "https://www.youtube.com/watch?v=mrUwlj3Mi58", min_steer_speed=3. * CV.MPH_TO_MS), + HondaCarInfo("Honda Accord 2018-22", "All", video_link="https://www.youtube.com/watch?v=mrUwlj3Mi58", min_steer_speed=3. * CV.MPH_TO_MS), HondaCarInfo("Honda Inspire 2018", "All", min_steer_speed=3. * CV.MPH_TO_MS), ], CAR.ACCORDH: HondaCarInfo("Honda Accord Hybrid 2018-22", "All", min_steer_speed=3. * CV.MPH_TO_MS), CAR.CIVIC: HondaCarInfo("Honda Civic 2016-18", min_steer_speed=12. * CV.MPH_TO_MS, video_link="https://youtu.be/-IkImTe1NYE"), CAR.CIVIC_BOSCH: [ - HondaCarInfo("Honda Civic 2019-21", "All", "https://www.youtube.com/watch?v=4Iz1Mz5LGF8", [Footnote.CIVIC_DIESEL], 2. * CV.MPH_TO_MS), + HondaCarInfo("Honda Civic 2019-21", "All", video_link="https://www.youtube.com/watch?v=4Iz1Mz5LGF8", footnotes=[Footnote.CIVIC_DIESEL], min_steer_speed=2. * CV.MPH_TO_MS), HondaCarInfo("Honda Civic Hatchback 2017-21", min_steer_speed=12. * CV.MPH_TO_MS), ], CAR.CIVIC_BOSCH_DIESEL: None, # same platform CAR.CIVIC_2022: [ - HondaCarInfo("Honda Civic 2022", "All"), - HondaCarInfo("Honda Civic Hatchback 2022", "All"), + HondaCarInfo("Honda Civic 2022", "All", video_link="https://youtu.be/ytiOT5lcp6Q"), + HondaCarInfo("Honda Civic Hatchback 2022", "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), @@ -138,6 +139,7 @@ CAR_INFO: Dict[str, Optional[Union[HondaCarInfo, List[HondaCarInfo]]]] = { CAR.FIT: HondaCarInfo("Honda Fit 2018-20", min_steer_speed=12. * CV.MPH_TO_MS), CAR.FREED: HondaCarInfo("Honda Freed 2020", min_steer_speed=12. * CV.MPH_TO_MS), CAR.HRV: HondaCarInfo("Honda HR-V 2019-22", min_steer_speed=12. * CV.MPH_TO_MS), + CAR.HRV_3G: HondaCarInfo("Honda HR-V 2023", "All"), CAR.ODYSSEY: HondaCarInfo("Honda Odyssey 2018-20"), CAR.ODYSSEY_CHN: None, # Chinese version of Odyssey CAR.ACURA_RDX: HondaCarInfo("Acura RDX 2016-18", "AcuraWatch Plus", min_steer_speed=12. * CV.MPH_TO_MS), @@ -146,7 +148,7 @@ CAR_INFO: Dict[str, Optional[Union[HondaCarInfo, List[HondaCarInfo]]]] = { HondaCarInfo("Honda Pilot 2016-22", min_steer_speed=12. * CV.MPH_TO_MS), HondaCarInfo("Honda Passport 2019-21", "All", min_steer_speed=12. * CV.MPH_TO_MS), ], - CAR.RIDGELINE: HondaCarInfo("Honda Ridgeline 2017-22", min_steer_speed=12. * CV.MPH_TO_MS), + CAR.RIDGELINE: HondaCarInfo("Honda Ridgeline 2017-23", min_steer_speed=12. * CV.MPH_TO_MS), CAR.INSIGHT: HondaCarInfo("Honda Insight 2019-22", "All", min_steer_speed=3. * CV.MPH_TO_MS), CAR.HONDA_E: HondaCarInfo("Honda e 2020", "All", min_steer_speed=3. * CV.MPH_TO_MS), } @@ -186,7 +188,7 @@ FW_QUERY_CONFIG = FwQueryConfig( [StdQueries.UDS_VERSION_RESPONSE], bus=1, logging=True, - non_obd=True, + obd_multiplexing=False, ), ], extra_ecus=[ @@ -1246,6 +1248,7 @@ FW_VERSIONS = { b'37805-5YF-A750\x00\x00', b'37805-5YF-A850\x00\x00', b'37805-5YF-A870\x00\x00', + b'37805-5YF-AD20\x00\x00', b'37805-5YF-C210\x00\x00', b'37805-5YF-C220\x00\x00', b'37805-5YF-C410\000\000', @@ -1254,16 +1257,20 @@ FW_VERSIONS = { (Ecu.vsa, 0x18da28f1, None): [ b'57114-TJB-A030\x00\x00', b'57114-TJB-A040\x00\x00', + b'57114-TJB-A120\x00\x00', ], (Ecu.fwdRadar, 0x18dab0f1, None): [ b'36802-TJB-A040\x00\x00', b'36802-TJB-A050\x00\x00', + b'36802-TJB-A540\x00\x00', ], (Ecu.fwdCamera, 0x18dab5f1, None): [ b'36161-TJB-A040\x00\x00', + b'36161-TJB-A530\x00\x00', ], (Ecu.shiftByWire, 0x18da0bf1, None): [ b'54008-TJB-A520\x00\x00', + b'54008-TJB-A530\x00\x00', ], (Ecu.transmission, 0x18da1ef1, None): [ b'28102-5YK-A610\x00\x00', @@ -1271,6 +1278,7 @@ FW_VERSIONS = { b'28102-5YK-A630\x00\x00', b'28102-5YK-A700\x00\x00', b'28102-5YK-A711\x00\x00', + b'28102-5YK-A800\x00\x00', b'28102-5YL-A620\x00\x00', b'28102-5YL-A700\x00\x00', b'28102-5YL-A711\x00\x00', @@ -1282,6 +1290,7 @@ FW_VERSIONS = { b'78109-TJB-AB10\x00\x00', b'78109-TJB-AD10\x00\x00', b'78109-TJB-AF10\x00\x00', + b'78109-TJB-AQ20\x00\x00', b'78109-TJB-AR10\x00\x00', b'78109-TJB-AS10\000\000', b'78109-TJB-AU10\x00\x00', @@ -1293,22 +1302,26 @@ FW_VERSIONS = { ], (Ecu.srs, 0x18da53f1, None): [ b'77959-TJB-A040\x00\x00', + b'77959-TJB-A120\x00\x00', b'77959-TJB-A210\x00\x00', ], (Ecu.electricBrakeBooster, 0x18da2bf1, None): [ b'46114-TJB-A040\x00\x00', b'46114-TJB-A050\x00\x00', b'46114-TJB-A060\x00\x00', + b'46114-TJB-A120\x00\x00', ], (Ecu.gateway, 0x18daeff1, None): [ b'38897-TJB-A040\x00\x00', b'38897-TJB-A110\x00\x00', b'38897-TJB-A120\x00\x00', + b'38897-TJB-A220\x00\x00', ], (Ecu.eps, 0x18da30f1, None): [ b'39990-TJB-A030\x00\x00', b'39990-TJB-A040\x00\x00', - b'39990-TJB-A130\x00\x00' + b'39990-TJB-A070\x00\x00', + b'39990-TJB-A130\x00\x00', ], }, CAR.RIDGELINE: { @@ -1408,6 +1421,32 @@ FW_VERSIONS = { b'78109-THW-A110\x00\x00', ], }, + CAR.HRV_3G: { + (Ecu.eps, 0x18DA30F1, None): [ + b'39990-3W0-A030\x00\x00', + ], + (Ecu.gateway, 0x18DAEFF1, None): [ + b'38897-3W1-A010\x00\x00', + ], + (Ecu.srs, 0x18DA53F1, None): [ + b'77959-3V0-A820\x00\x00', + ], + (Ecu.combinationMeter, 0x18DA60F1, None): [ + b'78108-3V1-A220\x00\x00', + ], + (Ecu.vsa, 0x18DA28F1, None): [ + b'57114-3W0-A040\x00\x00', + ], + (Ecu.transmission, 0x18DA1EF1, None): [ + b'28101-6EH-A010\x00\x00', + ], + (Ecu.programmedFuelInjection, 0x18DA10F1, None): [ + b'37805-6CT-A710\x00\x00', + ], + (Ecu.electricBrakeBooster, 0x18DA2BF1, None): [ + b'46114-3W0-A020\x00\x00', + ], + }, CAR.ACURA_ILX: { (Ecu.gateway, 0x18daeff1, None): [ b'38897-TX6-A010\x00\x00', @@ -1526,6 +1565,7 @@ DBC = { CAR.FIT: dbc_dict('honda_fit_ex_2018_can_generated', 'acura_ilx_2016_nidec'), CAR.FREED: dbc_dict('honda_fit_ex_2018_can_generated', 'acura_ilx_2016_nidec'), CAR.HRV: dbc_dict('honda_fit_ex_2018_can_generated', 'acura_ilx_2016_nidec'), + CAR.HRV_3G: dbc_dict('honda_civic_ex_2022_can_generated', None), CAR.ODYSSEY: dbc_dict('honda_odyssey_exl_2018_generated', 'acura_ilx_2016_nidec'), CAR.ODYSSEY_CHN: dbc_dict('honda_odyssey_extreme_edition_2018_china_can_generated', 'acura_ilx_2016_nidec'), CAR.PILOT: dbc_dict('acura_ilx_2016_can_generated', 'acura_ilx_2016_nidec'), @@ -1545,6 +1585,6 @@ HONDA_NIDEC_ALT_PCM_ACCEL = {CAR.ODYSSEY} HONDA_NIDEC_ALT_SCM_MESSAGES = {CAR.ACURA_ILX, CAR.ACURA_RDX, CAR.CRV, CAR.CRV_EU, CAR.FIT, CAR.FREED, CAR.HRV, CAR.ODYSSEY_CHN, CAR.PILOT, CAR.RIDGELINE} HONDA_BOSCH = {CAR.ACCORD, CAR.ACCORDH, CAR.CIVIC_BOSCH, CAR.CIVIC_BOSCH_DIESEL, CAR.CRV_5G, - CAR.CRV_HYBRID, CAR.INSIGHT, CAR.ACURA_RDX_3G, CAR.HONDA_E, CAR.CIVIC_2022} -HONDA_BOSCH_ALT_BRAKE_SIGNAL = {CAR.ACCORD, CAR.CRV_5G, CAR.ACURA_RDX_3G} -HONDA_BOSCH_RADARLESS = {CAR.CIVIC_2022} + CAR.CRV_HYBRID, CAR.INSIGHT, CAR.ACURA_RDX_3G, CAR.HONDA_E, CAR.CIVIC_2022, CAR.HRV_3G} +HONDA_BOSCH_ALT_BRAKE_SIGNAL = {CAR.ACCORD, CAR.CRV_5G, CAR.ACURA_RDX_3G, CAR.HRV_3G} +HONDA_BOSCH_RADARLESS = {CAR.CIVIC_2022, CAR.HRV_3G} diff --git a/selfdrive/car/hyundai/carcontroller.py b/selfdrive/car/hyundai/carcontroller.py index 2572038492..ac74d2cc5b 100644 --- a/selfdrive/car/hyundai/carcontroller.py +++ b/selfdrive/car/hyundai/carcontroller.py @@ -5,6 +5,7 @@ from common.realtime import DT_CTRL from opendbc.can.packer import CANPacker from selfdrive.car import apply_driver_steer_torque_limits from selfdrive.car.hyundai import hyundaicanfd, hyundaican +from selfdrive.car.hyundai.hyundaicanfd import CanBus from selfdrive.car.hyundai.values import HyundaiFlags, Buttons, CarControllerParams, CANFD_CAR, CAR VisualAlert = car.CarControl.HUDControl.VisualAlert @@ -44,6 +45,7 @@ def process_hud_alert(enabled, fingerprint, hud_control): class CarController: def __init__(self, dbc_name, CP, VM): self.CP = CP + self.CAN = CanBus(CP) self.params = CarControllerParams(CP) self.packer = CANPacker(dbc_name) self.angle_limit_counter = 0 @@ -85,12 +87,12 @@ class CarController: # for longitudinal control, either radar or ADAS driving ECU addr, bus = 0x7d0, 0 if self.CP.flags & HyundaiFlags.CANFD_HDA2.value: - addr, bus = 0x730, 5 + addr, bus = 0x730, self.CAN.ECAN can_sends.append([addr, 0, b"\x02\x3E\x80\x00\x00\x00\x00\x00", bus]) # for blinkers if self.CP.flags & HyundaiFlags.ENABLE_BLINKERS: - can_sends.append([0x7b1, 0, b"\x02\x3E\x80\x00\x00\x00\x00\x00", 5]) + can_sends.append([0x7b1, 0, b"\x02\x3E\x80\x00\x00\x00\x00\x00", self.CAN.ECAN]) # >90 degree steering fault prevention # Count up to MAX_ANGLE_FRAMES, at which point we need to cut torque to avoid a steering fault @@ -112,25 +114,25 @@ class CarController: hda2_long = hda2 and self.CP.openpilotLongitudinalControl # steering control - can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, CC.enabled, lat_active, apply_steer)) + can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, self.CAN, CC.enabled, lat_active, apply_steer)) # disable LFA on HDA2 if self.frame % 5 == 0 and hda2: - can_sends.append(hyundaicanfd.create_cam_0x2a4(self.packer, CS.cam_0x2a4)) + can_sends.append(hyundaicanfd.create_cam_0x2a4(self.packer, self.CAN, CS.cam_0x2a4)) # LFA and HDA icons if self.frame % 5 == 0 and (not hda2 or hda2_long): - can_sends.append(hyundaicanfd.create_lfahda_cluster(self.packer, self.CP, CC.enabled)) + can_sends.append(hyundaicanfd.create_lfahda_cluster(self.packer, self.CAN, CC.enabled)) # blinkers if hda2 and self.CP.flags & HyundaiFlags.ENABLE_BLINKERS: - can_sends.extend(hyundaicanfd.create_spas_messages(self.packer, self.frame, CC.leftBlinker, CC.rightBlinker)) + can_sends.extend(hyundaicanfd.create_spas_messages(self.packer, self.CAN, self.frame, CC.leftBlinker, CC.rightBlinker)) if self.CP.openpilotLongitudinalControl: if hda2: - can_sends.extend(hyundaicanfd.create_adrv_messages(self.packer, self.frame)) + can_sends.extend(hyundaicanfd.create_adrv_messages(self.packer, self.CAN, self.frame)) if self.frame % 2 == 0: - can_sends.append(hyundaicanfd.create_acc_control(self.packer, self.CP, CC.enabled, self.accel_last, accel, stopping, CC.cruiseControl.override, + can_sends.append(hyundaicanfd.create_acc_control(self.packer, self.CAN, CC.enabled, self.accel_last, accel, stopping, CC.cruiseControl.override, set_speed_in_units)) self.accel_last = accel else: @@ -139,11 +141,11 @@ class CarController: # cruise cancel if CC.cruiseControl.cancel: if self.CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS: - can_sends.append(hyundaicanfd.create_acc_cancel(self.packer, self.CP, CS.cruise_info)) + can_sends.append(hyundaicanfd.create_acc_cancel(self.packer, self.CAN, CS.cruise_info)) self.last_button_frame = self.frame else: for _ in range(20): - can_sends.append(hyundaicanfd.create_buttons(self.packer, self.CP, CS.buttons_counter+1, Buttons.CANCEL)) + can_sends.append(hyundaicanfd.create_buttons(self.packer, self.CP, self.CAN, CS.buttons_counter+1, Buttons.CANCEL)) self.last_button_frame = self.frame # cruise standstill resume @@ -153,7 +155,7 @@ class CarController: pass else: for _ in range(20): - can_sends.append(hyundaicanfd.create_buttons(self.packer, self.CP, CS.buttons_counter+1, Buttons.RES_ACCEL)) + can_sends.append(hyundaicanfd.create_buttons(self.packer, self.CP, self.CAN, CS.buttons_counter+1, Buttons.RES_ACCEL)) self.last_button_frame = self.frame else: can_sends.append(hyundaican.create_lkas11(self.packer, self.frame, self.car_fingerprint, apply_steer, lat_active, diff --git a/selfdrive/car/hyundai/carstate.py b/selfdrive/car/hyundai/carstate.py index 22934c05b2..cec2c3e69e 100644 --- a/selfdrive/car/hyundai/carstate.py +++ b/selfdrive/car/hyundai/carstate.py @@ -6,8 +6,8 @@ from cereal import car from common.conversions import Conversions as CV from opendbc.can.parser import CANParser from opendbc.can.can_define import CANDefine -from selfdrive.car.hyundai.hyundaicanfd import get_e_can_bus -from selfdrive.car.hyundai.values import HyundaiFlags, CAR, DBC, FEATURES, CAMERA_SCC_CAR, CANFD_CAR, EV_CAR, HYBRID_CAR, Buttons, CarControllerParams +from selfdrive.car.hyundai.hyundaicanfd import CanBus +from selfdrive.car.hyundai.values import HyundaiFlags, CAR, DBC, CAN_GEARS, CAMERA_SCC_CAR, CANFD_CAR, EV_CAR, HYBRID_CAR, Buttons, CarControllerParams from selfdrive.car.interfaces import CarStateBase PREV_BUTTON_SAMPLES = 8 @@ -27,9 +27,9 @@ class CarState(CarStateBase): "GEAR_SHIFTER" if CP.carFingerprint in CANFD_CAR: self.shifter_values = can_define.dv[self.gear_msg_canfd]["GEAR"] - elif self.CP.carFingerprint in FEATURES["use_cluster_gears"]: + elif self.CP.carFingerprint in CAN_GEARS["use_cluster_gears"]: self.shifter_values = can_define.dv["CLU15"]["CF_Clu_Gear"] - elif self.CP.carFingerprint in FEATURES["use_tcu_gears"]: + elif self.CP.carFingerprint in CAN_GEARS["use_tcu_gears"]: self.shifter_values = can_define.dv["TCU12"]["CUR_GR"] else: # preferred and elect gear methods use same definition self.shifter_values = can_define.dv["LVR12"]["CF_Lvr_Gear"] @@ -123,11 +123,11 @@ class CarState(CarStateBase): # Gear Selection via Cluster - For those Kia/Hyundai which are not fully discovered, we can use the Cluster Indicator for Gear Selection, # as this seems to be standard over all cars, but is not the preferred method. - if self.CP.carFingerprint in FEATURES["use_cluster_gears"]: + if self.CP.carFingerprint in CAN_GEARS["use_cluster_gears"]: gear = cp.vl["CLU15"]["CF_Clu_Gear"] - elif self.CP.carFingerprint in FEATURES["use_tcu_gears"]: + elif self.CP.carFingerprint in CAN_GEARS["use_tcu_gears"]: gear = cp.vl["TCU12"]["CUR_GR"] - elif self.CP.carFingerprint in FEATURES["use_elect_gears"]: + elif self.CP.carFingerprint in CAN_GEARS["use_elect_gears"]: gear = cp.vl["ELECT_GEAR"]["Elect_Gear_Shifter"] else: gear = cp.vl["LVR12"]["CF_Lvr_Gear"] @@ -354,12 +354,12 @@ class CarState(CarStateBase): ("EMS16", 100), ] - if CP.carFingerprint in FEATURES["use_cluster_gears"]: + if CP.carFingerprint in CAN_GEARS["use_cluster_gears"]: signals.append(("CF_Clu_Gear", "CLU15")) - elif CP.carFingerprint in FEATURES["use_tcu_gears"]: + elif CP.carFingerprint in CAN_GEARS["use_tcu_gears"]: signals.append(("CUR_GR", "TCU12")) checks.append(("TCU12", 100)) - elif CP.carFingerprint in FEATURES["use_elect_gears"]: + elif CP.carFingerprint in CAN_GEARS["use_elect_gears"]: signals.append(("Elect_Gear_Shifter", "ELECT_GEAR")) checks.append(("ELECT_GEAR", 20)) else: @@ -516,7 +516,7 @@ class CarState(CarStateBase): ("ACCELERATOR_BRAKE_ALT", 100), ] - return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, get_e_can_bus(CP)) + return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, CanBus(CP).ECAN) @staticmethod def get_cam_can_parser_canfd(CP): @@ -543,4 +543,4 @@ class CarState(CarStateBase): ("SCC_CONTROL", 50), ] - return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, 6) + return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, CanBus(CP).CAM) diff --git a/selfdrive/car/hyundai/hyundaicanfd.py b/selfdrive/car/hyundai/hyundaicanfd.py index af7239571c..3717a45909 100644 --- a/selfdrive/car/hyundai/hyundaicanfd.py +++ b/selfdrive/car/hyundai/hyundaicanfd.py @@ -1,14 +1,44 @@ +import math + from common.numpy_fast import clip from selfdrive.car.hyundai.values import HyundaiFlags -def get_e_can_bus(CP): - # On the CAN-FD platforms, the LKAS camera is on both A-CAN and E-CAN. HDA2 cars - # have a different harness than the HDA1 and non-HDA variants in order to split - # a different bus, since the steering is done by different ECUs. - return 5 if CP.flags & HyundaiFlags.CANFD_HDA2 else 4 + +class CanBus: + def __init__(self, CP, hda2=None, fingerprint=None): + if CP is None: + assert None not in (hda2, fingerprint) + num = math.ceil(max([k for k, v in fingerprint.items() if len(v)], default=1) / 4) + else: + hda2 = CP.flags & HyundaiFlags.CANFD_HDA2.value + num = len(CP.safetyConfigs) + + # On the CAN-FD platforms, the LKAS camera is on both A-CAN and E-CAN. HDA2 cars + # have a different harness than the HDA1 and non-HDA variants in order to split + # a different bus, since the steering is done by different ECUs. + self._a, self._e = 1, 0 + if hda2: + self._a, self._e = 0, 1 + + offset = 4*(num - 1) + self._a += offset + self._e += offset + self._cam = 2 + offset + + @property + def ECAN(self): + return self._e + + @property + def ACAN(self): + return self._a + + @property + def CAM(self): + return self._cam -def create_steering_messages(packer, CP, enabled, lat_active, apply_steer): +def create_steering_messages(packer, CP, CAN, enabled, lat_active, apply_steer): ret = [] @@ -26,45 +56,45 @@ def create_steering_messages(packer, CP, enabled, lat_active, apply_steer): if CP.flags & HyundaiFlags.CANFD_HDA2: if CP.openpilotLongitudinalControl: - ret.append(packer.make_can_msg("LFA", 5, values)) - ret.append(packer.make_can_msg("LKAS", 4, values)) + ret.append(packer.make_can_msg("LFA", CAN.ECAN, values)) + ret.append(packer.make_can_msg("LKAS", CAN.ACAN, values)) else: - ret.append(packer.make_can_msg("LFA", 4, values)) + ret.append(packer.make_can_msg("LFA", CAN.ECAN, values)) return ret -def create_cam_0x2a4(packer, camera_values): +def create_cam_0x2a4(packer, CAN, camera_values): camera_values.update({ "BYTE7": 0, }) - return packer.make_can_msg("CAM_0x2a4", 4, camera_values) + return packer.make_can_msg("CAM_0x2a4", CAN.ACAN, camera_values) -def create_buttons(packer, CP, cnt, btn): +def create_buttons(packer, CP, CAN, cnt, btn): values = { "COUNTER": cnt, "SET_ME_1": 1, "CRUISE_BUTTONS": btn, } - bus = 5 if CP.flags & HyundaiFlags.CANFD_HDA2 else 6 + bus = CAN.ECAN if CP.flags & HyundaiFlags.CANFD_HDA2 else CAN.CAM return packer.make_can_msg("CRUISE_BUTTONS", bus, values) -def create_acc_cancel(packer, CP, cruise_info_copy): +def create_acc_cancel(packer, CAN, cruise_info_copy): values = cruise_info_copy values.update({ "ACCMode": 4, }) - return packer.make_can_msg("SCC_CONTROL", get_e_can_bus(CP), values) + return packer.make_can_msg("SCC_CONTROL", CAN.ECAN, values) -def create_lfahda_cluster(packer, CP, enabled): +def create_lfahda_cluster(packer, CAN, enabled): values = { "HDA_ICON": 1 if enabled else 0, "LFA_ICON": 2 if enabled else 0, } - return packer.make_can_msg("LFAHDA_CLUSTER", get_e_can_bus(CP), values) + return packer.make_can_msg("LFAHDA_CLUSTER", CAN.ECAN, values) -def create_acc_control(packer, CP, enabled, accel_last, accel, stopping, gas_override, set_speed): +def create_acc_control(packer, CAN, enabled, accel_last, accel, stopping, gas_override, set_speed): jerk = 5 jn = jerk / 50 if not enabled or gas_override: @@ -92,15 +122,15 @@ def create_acc_control(packer, CP, enabled, accel_last, accel, stopping, gas_ove "DISTANCE_SETTING": 4, } - return packer.make_can_msg("SCC_CONTROL", get_e_can_bus(CP), values) + return packer.make_can_msg("SCC_CONTROL", CAN.ECAN, values) -def create_spas_messages(packer, frame, left_blink, right_blink): +def create_spas_messages(packer, CAN, frame, left_blink, right_blink): ret = [] values = { } - ret.append(packer.make_can_msg("SPAS1", 5, values)) + ret.append(packer.make_can_msg("SPAS1", CAN.ECAN, values)) blink = 0 if left_blink: @@ -110,12 +140,12 @@ def create_spas_messages(packer, frame, left_blink, right_blink): values = { "BLINKER_CONTROL": blink, } - ret.append(packer.make_can_msg("SPAS2", 5, values)) + ret.append(packer.make_can_msg("SPAS2", CAN.ECAN, values)) return ret -def create_adrv_messages(packer, frame): +def create_adrv_messages(packer, CAN, frame): # messages needed to car happy after disabling # the ADAS Driving ECU to do longitudinal control @@ -123,7 +153,7 @@ def create_adrv_messages(packer, frame): values = { } - ret.append(packer.make_can_msg("ADRV_0x51", 4, values)) + ret.append(packer.make_can_msg("ADRV_0x51", CAN.ACAN, values)) if frame % 2 == 0: values = { @@ -133,7 +163,7 @@ def create_adrv_messages(packer, frame): 'SET_ME_FC': 0xfc, 'SET_ME_9': 0x9, } - ret.append(packer.make_can_msg("ADRV_0x160", 5, values)) + ret.append(packer.make_can_msg("ADRV_0x160", CAN.ECAN, values)) if frame % 5 == 0: values = { @@ -142,25 +172,25 @@ def create_adrv_messages(packer, frame): 'SET_ME_TMP_F': 0xf, 'SET_ME_TMP_F_2': 0xf, } - ret.append(packer.make_can_msg("ADRV_0x1ea", 5, values)) + ret.append(packer.make_can_msg("ADRV_0x1ea", CAN.ECAN, values)) values = { 'SET_ME_E1': 0xe1, 'SET_ME_3A': 0x3a, } - ret.append(packer.make_can_msg("ADRV_0x200", 5, values)) + ret.append(packer.make_can_msg("ADRV_0x200", CAN.ECAN, values)) if frame % 20 == 0: values = { 'SET_ME_15': 0x15, } - ret.append(packer.make_can_msg("ADRV_0x345", 5, values)) + ret.append(packer.make_can_msg("ADRV_0x345", CAN.ECAN, values)) if frame % 100 == 0: values = { 'SET_ME_22': 0x22, 'SET_ME_41': 0x41, } - ret.append(packer.make_can_msg("ADRV_0x1da", 5, values)) + ret.append(packer.make_can_msg("ADRV_0x1da", CAN.ECAN, values)) return ret diff --git a/selfdrive/car/hyundai/interface.py b/selfdrive/car/hyundai/interface.py index 1dd91ac888..3363ea8eea 100644 --- a/selfdrive/car/hyundai/interface.py +++ b/selfdrive/car/hyundai/interface.py @@ -2,7 +2,7 @@ from cereal import car from panda import Panda from common.conversions import Conversions as CV -from selfdrive.car.hyundai.hyundaicanfd import get_e_can_bus +from selfdrive.car.hyundai.hyundaicanfd import CanBus from selfdrive.car.hyundai.values import HyundaiFlags, CAR, DBC, CANFD_CAR, CAMERA_SCC_CAR, CANFD_RADAR_SCC_CAR, EV_CAR, HYBRID_CAR, LEGACY_SAFETY_MODE_CAR, Buttons from selfdrive.car.hyundai.radar_interface import RADAR_START_ADDR from selfdrive.car import STD_CARGO_KG, create_button_event, scale_tire_stiffness, get_safety_config @@ -19,7 +19,7 @@ BUTTONS_DICT = {Buttons.RES_ACCEL: ButtonType.accelCruise, Buttons.SET_DECEL: Bu class CarInterface(CarInterfaceBase): @staticmethod - def _get_params(ret, candidate, fingerprint, car_fw, experimental_long): + def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs): ret.carName = "hyundai" ret.radarUnavailable = RADAR_START_ADDR not in fingerprint[1] or DBC[ret.carFingerprint]["radar"] is None @@ -28,17 +28,20 @@ class CarInterface(CarInterfaceBase): # added to selfdrive/car/tests/routes.py, we can remove it from this list. ret.dashcamOnly = candidate in {CAR.KIA_OPTIMA_H, } + hda2 = Ecu.adas in [fw.ecu for fw in car_fw] + CAN = CanBus(None, hda2, fingerprint) + if candidate in CANFD_CAR: # detect HDA2 with ADAS Driving ECU - if Ecu.adas in [fw.ecu for fw in car_fw]: + if hda2: ret.flags |= HyundaiFlags.CANFD_HDA2.value else: # non-HDA2 - if 0x1cf not in fingerprint[4]: + if 0x1cf not in fingerprint[CAN.ECAN]: ret.flags |= HyundaiFlags.CANFD_ALT_BUTTONS.value # ICE cars do not have 0x130; GEARS message on 0x40 or 0x70 instead - if 0x130 not in fingerprint[4]: - if 0x40 not in fingerprint[4]: + if 0x130 not in fingerprint[CAN.ECAN]: + if 0x40 not in fingerprint[CAN.ECAN]: ret.flags |= HyundaiFlags.CANFD_ALT_GEARS_2.value else: ret.flags |= HyundaiFlags.CANFD_ALT_GEARS.value @@ -136,10 +139,10 @@ class CarInterface(CarInterfaceBase): ret.mass = 1985. + STD_CARGO_KG ret.wheelbase = 2.78 ret.steerRatio = 14.4 * 1.1 # 10% higher at the center seems reasonable - elif candidate in (CAR.KIA_NIRO_EV, CAR.KIA_NIRO_PHEV, CAR.KIA_NIRO_HEV_2021, CAR.KIA_NIRO_HEV_2ND_GEN): - ret.mass = 3452. * CV.LB_TO_KG + STD_CARGO_KG # average of all the cars + elif candidate in (CAR.KIA_NIRO_EV, CAR.KIA_NIRO_EV_2ND_GEN, CAR.KIA_NIRO_PHEV, CAR.KIA_NIRO_HEV_2021, CAR.KIA_NIRO_HEV_2ND_GEN): + ret.mass = 3543. * CV.LB_TO_KG + STD_CARGO_KG # average of all the cars ret.wheelbase = 2.7 - ret.steerRatio = 13.9 if CAR.KIA_NIRO_HEV_2021 else 13.73 # Spec + ret.steerRatio = 13.6 # average of all the cars tire_stiffness_factor = 0.385 if candidate == CAR.KIA_NIRO_PHEV: ret.minSteerSpeed = 32 * CV.MPH_TO_MS @@ -248,21 +251,23 @@ class CarInterface(CarInterfaceBase): # *** feature detection *** if candidate in CANFD_CAR: - ret.enableBsm = 0x1e5 in fingerprint[get_e_can_bus(ret)] + ret.enableBsm = 0x1e5 in fingerprint[CAN.ECAN] else: ret.enableBsm = 0x58b in fingerprint[0] # *** panda safety config *** if candidate in CANFD_CAR: - ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.noOutput), - get_safety_config(car.CarParams.SafetyModel.hyundaiCanfd)] + cfgs = [get_safety_config(car.CarParams.SafetyModel.hyundaiCanfd), ] + if CAN.ECAN >= 4: + cfgs.insert(0, get_safety_config(car.CarParams.SafetyModel.noOutput)) + ret.safetyConfigs = cfgs if ret.flags & HyundaiFlags.CANFD_HDA2: - ret.safetyConfigs[1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_HDA2 + ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_HDA2 if ret.flags & HyundaiFlags.CANFD_ALT_BUTTONS: - ret.safetyConfigs[1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_ALT_BUTTONS + ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_ALT_BUTTONS if ret.flags & HyundaiFlags.CANFD_CAMERA_SCC: - ret.safetyConfigs[1].safetyParam |= Panda.FLAG_HYUNDAI_CAMERA_SCC + ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_CAMERA_SCC else: if candidate in LEGACY_SAFETY_MODE_CAR: # these cars require a special panda safety mode due to missing counters and checksums in the messages @@ -297,12 +302,12 @@ class CarInterface(CarInterfaceBase): if CP.openpilotLongitudinalControl and not (CP.flags & HyundaiFlags.CANFD_CAMERA_SCC.value): addr, bus = 0x7d0, 0 if CP.flags & HyundaiFlags.CANFD_HDA2.value: - addr, bus = 0x730, 5 + addr, bus = 0x730, CanBus(CP).ECAN disable_ecu(logcan, sendcan, bus=bus, addr=addr, com_cont_req=b'\x28\x83\x01') # for blinkers if CP.flags & HyundaiFlags.ENABLE_BLINKERS: - disable_ecu(logcan, sendcan, bus=5, addr=0x7B1, com_cont_req=b'\x28\x83\x01') + disable_ecu(logcan, sendcan, bus=CanBus(CP.ECAN), addr=0x7B1, com_cont_req=b'\x28\x83\x01') def _update(self, c): ret = self.CS.update(self.cp, self.cp_cam) diff --git a/selfdrive/car/hyundai/tests/test_hyundai.py b/selfdrive/car/hyundai/tests/test_hyundai.py index 6ecfa03796..fba99ae74d 100755 --- a/selfdrive/car/hyundai/tests/test_hyundai.py +++ b/selfdrive/car/hyundai/tests/test_hyundai.py @@ -2,16 +2,21 @@ import unittest from cereal import car -from selfdrive.car.hyundai.values import CANFD_CAR, FW_QUERY_CONFIG, FW_VERSIONS +from selfdrive.car.hyundai.values import CANFD_CAR, FW_QUERY_CONFIG, FW_VERSIONS, CAN_GEARS, LEGACY_SAFETY_MODE_CAR, CHECKSUM, CAMERA_SCC_CAR Ecu = car.CarParams.Ecu ECU_NAME = {v: k for k, v in Ecu.schema.enumerants.items()} class TestHyundaiFingerprint(unittest.TestCase): + def test_canfd_not_in_can_features(self): + can_specific_feature_list = set.union(*CAN_GEARS.values(), *CHECKSUM.values(), LEGACY_SAFETY_MODE_CAR, CAMERA_SCC_CAR) + for car_model in CANFD_CAR: + self.assertNotIn(car_model, can_specific_feature_list, "CAN FD car unexpectedly found in a CAN feature list") + def test_auxiliary_request_ecu_whitelist(self): # Asserts only auxiliary Ecus can exist in database for CAN-FD cars - whitelisted_ecus = {ecu for r in FW_QUERY_CONFIG.requests for ecu in r.whitelist_ecus if r.bus > 3} + whitelisted_ecus = {ecu for r in FW_QUERY_CONFIG.requests for ecu in r.whitelist_ecus if r.auxiliary} for car_model in CANFD_CAR: ecus = {fw[0] for fw in FW_VERSIONS[car_model].keys()} diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 49e4ce033d..efd1578135 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -100,6 +100,7 @@ class CAR: KIA_K5_2021 = "KIA K5 2021" KIA_K5_HEV_2020 = "KIA K5 HYBRID 2020" KIA_NIRO_EV = "KIA NIRO EV 2020" + KIA_NIRO_EV_2ND_GEN = "KIA NIRO EV 2ND GEN" KIA_NIRO_PHEV = "KIA NIRO HYBRID 2019" KIA_NIRO_HEV_2021 = "KIA NIRO HYBRID 2021" KIA_NIRO_HEV_2ND_GEN = "KIA NIRO HYBRID 2ND GEN" @@ -160,23 +161,23 @@ CAR_INFO: Dict[str, Optional[Union[HyundaiCarInfo, List[HyundaiCarInfo]]]] = { CAR.IONIQ_EV_LTD: HyundaiCarInfo("Hyundai Ioniq Electric 2019", harness=Harness.hyundai_c), CAR.IONIQ_EV_2020: HyundaiCarInfo("Hyundai Ioniq Electric 2020", "All", harness=Harness.hyundai_h), CAR.IONIQ_PHEV_2019: HyundaiCarInfo("Hyundai Ioniq Plug-in Hybrid 2019", harness=Harness.hyundai_c), - CAR.IONIQ_PHEV: HyundaiCarInfo("Hyundai Ioniq Plug-in Hybrid 2020-21", "All", harness=Harness.hyundai_h), + CAR.IONIQ_PHEV: HyundaiCarInfo("Hyundai Ioniq Plug-in Hybrid 2020-22", "All", harness=Harness.hyundai_h), CAR.KONA: HyundaiCarInfo("Hyundai Kona 2020", harness=Harness.hyundai_b), CAR.KONA_EV: HyundaiCarInfo("Hyundai Kona Electric 2018-21", harness=Harness.hyundai_g), CAR.KONA_EV_2022: HyundaiCarInfo("Hyundai Kona Electric 2022", harness=Harness.hyundai_o), CAR.KONA_HEV: HyundaiCarInfo("Hyundai Kona Hybrid 2020", video_link="https://youtu.be/0dwpAHiZgFo", harness=Harness.hyundai_i), # TODO: check packages CAR.SANTA_FE: HyundaiCarInfo("Hyundai Santa Fe 2019-20", "All", harness=Harness.hyundai_d), - CAR.SANTA_FE_2022: HyundaiCarInfo("Hyundai Santa Fe 2021-22", "All", "https://youtu.be/VnHzSTygTS4", harness=Harness.hyundai_l), + CAR.SANTA_FE_2022: HyundaiCarInfo("Hyundai Santa Fe 2021-22", "All", video_link="https://youtu.be/VnHzSTygTS4", harness=Harness.hyundai_l), CAR.SANTA_FE_HEV_2022: HyundaiCarInfo("Hyundai Santa Fe Hybrid 2022", "All", harness=Harness.hyundai_l), CAR.SANTA_FE_PHEV_2022: HyundaiCarInfo("Hyundai Santa Fe Plug-in Hybrid 2022", "All", harness=Harness.hyundai_l), - CAR.SONATA: HyundaiCarInfo("Hyundai Sonata 2020-23", "All", "https://www.youtube.com/watch?v=ix63r9kE3Fw", harness=Harness.hyundai_a), + CAR.SONATA: HyundaiCarInfo("Hyundai Sonata 2020-23", "All", video_link="https://www.youtube.com/watch?v=ix63r9kE3Fw", harness=Harness.hyundai_a), CAR.SONATA_LF: HyundaiCarInfo("Hyundai Sonata 2018-19", harness=Harness.hyundai_e), CAR.TUCSON: [ HyundaiCarInfo("Hyundai Tucson 2021", min_enable_speed=19 * CV.MPH_TO_MS, harness=Harness.hyundai_l), HyundaiCarInfo("Hyundai Tucson Diesel 2019", harness=Harness.hyundai_l), ], CAR.PALISADE: [ - HyundaiCarInfo("Hyundai Palisade 2020-22", "All", "https://youtu.be/TAnDqjF4fDY?t=456", harness=Harness.hyundai_h), + HyundaiCarInfo("Hyundai Palisade 2020-22", "All", video_link="https://youtu.be/TAnDqjF4fDY?t=456", harness=Harness.hyundai_h), HyundaiCarInfo("Kia Telluride 2020-22", "All", harness=Harness.hyundai_h), ], CAR.VELOSTER: HyundaiCarInfo("Hyundai Veloster 2019-20", min_enable_speed=5. * CV.MPH_TO_MS, harness=Harness.hyundai_e), @@ -198,18 +199,18 @@ CAR_INFO: Dict[str, Optional[Union[HyundaiCarInfo, List[HyundaiCarInfo]]]] = { CAR.KIA_K5_2021: HyundaiCarInfo("Kia K5 2021-22", harness=Harness.hyundai_a), CAR.KIA_K5_HEV_2020: HyundaiCarInfo("Kia K5 Hybrid 2020", harness=Harness.hyundai_a), CAR.KIA_NIRO_EV: [ - HyundaiCarInfo("Kia Niro EV 2019", "All", "https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_h), - HyundaiCarInfo("Kia Niro EV 2020", "All", "https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_f), - HyundaiCarInfo("Kia Niro EV 2021", "All", "https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_c), - HyundaiCarInfo("Kia Niro EV 2022", "All", "https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_h), + HyundaiCarInfo("Kia Niro EV 2019", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_h), + HyundaiCarInfo("Kia Niro EV 2020", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_f), + HyundaiCarInfo("Kia Niro EV 2021", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_c), + HyundaiCarInfo("Kia Niro EV 2022", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_h), ], + CAR.KIA_NIRO_EV_2ND_GEN: HyundaiCarInfo("Kia Niro EV 2023", "All", harness=Harness.hyundai_a), CAR.KIA_NIRO_PHEV: [ HyundaiCarInfo("Kia Niro Plug-in Hybrid 2018-19", "All", min_enable_speed=10. * CV.MPH_TO_MS, harness=Harness.hyundai_c), HyundaiCarInfo("Kia Niro Plug-in Hybrid 2020", "All", harness=Harness.hyundai_d), ], CAR.KIA_NIRO_HEV_2021: [ - HyundaiCarInfo("Kia Niro Hybrid 2021", harness=Harness.hyundai_f), # TODO: could be hyundai_d, verify - HyundaiCarInfo("Kia Niro Hybrid 2022", harness=Harness.hyundai_h), + HyundaiCarInfo("Kia Niro Hybrid 2021-22", harness=Harness.hyundai_f), # TODO: 2021 could be hyundai_d, verify ], CAR.KIA_NIRO_HEV_2ND_GEN: HyundaiCarInfo("Kia Niro Hybrid 2023", harness=Harness.hyundai_a), CAR.KIA_OPTIMA_G4: HyundaiCarInfo("Kia Optima 2017", "Advanced Smart Cruise Control", harness=Harness.hyundai_b), # TODO: may support 2016, 2018 @@ -221,7 +222,7 @@ CAR_INFO: Dict[str, Optional[Union[HyundaiCarInfo, List[HyundaiCarInfo]]]] = { CAR.KIA_SELTOS: HyundaiCarInfo("Kia Seltos 2021", harness=Harness.hyundai_a), CAR.KIA_SPORTAGE_5TH_GEN: HyundaiCarInfo("Kia Sportage 2023", harness=Harness.hyundai_n), CAR.KIA_SORENTO: [ - HyundaiCarInfo("Kia Sorento 2018", "Advanced Smart Cruise Control", "https://www.youtube.com/watch?v=Fkh3s6WHJz8", harness=Harness.hyundai_c), + HyundaiCarInfo("Kia Sorento 2018", "Advanced Smart Cruise Control", video_link="https://www.youtube.com/watch?v=Fkh3s6WHJz8", harness=Harness.hyundai_c), HyundaiCarInfo("Kia Sorento 2019", video_link="https://www.youtube.com/watch?v=Fkh3s6WHJz8", harness=Harness.hyundai_e), ], CAR.KIA_SORENTO_4TH_GEN: HyundaiCarInfo("Kia Sorento 2021-23", harness=Harness.hyundai_k), @@ -232,8 +233,8 @@ CAR_INFO: Dict[str, Optional[Union[HyundaiCarInfo, List[HyundaiCarInfo]]]] = { CAR.KIA_CEED: HyundaiCarInfo("Kia Ceed 2019", harness=Harness.hyundai_e), CAR.KIA_EV6: [ HyundaiCarInfo("Kia EV6 (Southeast Asia only) 2022-23", "All", harness=Harness.hyundai_p), - HyundaiCarInfo("Kia EV6 (without HDA II) 2022", "Highway Driving Assist", harness=Harness.hyundai_l), - HyundaiCarInfo("Kia EV6 (with HDA II) 2022", "Highway Driving Assist II", harness=Harness.hyundai_p) + HyundaiCarInfo("Kia EV6 (without HDA II) 2022-23", "Highway Driving Assist", harness=Harness.hyundai_l), + HyundaiCarInfo("Kia EV6 (with HDA II) 2022-23", "Highway Driving Assist II", harness=Harness.hyundai_p) ], # Genesis @@ -335,10 +336,15 @@ FINGERPRINTS = { HYUNDAI_VERSION_REQUEST_LONG = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \ p16(0xf100) # Long description + +HYUNDAI_VERSION_REQUEST_ALT = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \ + p16(0xf110) # Alt long description + HYUNDAI_VERSION_REQUEST_MULTI = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \ p16(uds.DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_SPARE_PART_NUMBER) + \ p16(uds.DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION) + \ p16(0xf100) + HYUNDAI_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) FW_QUERY_CONFIG = FwQueryConfig( @@ -355,22 +361,46 @@ FW_QUERY_CONFIG = FwQueryConfig( [HYUNDAI_VERSION_RESPONSE], whitelist_ecus=[Ecu.engine, Ecu.transmission, Ecu.eps, Ecu.abs, Ecu.fwdRadar], ), - # CAN-FD queries (camera) + + # CAN-FD queries (from camera) + # TODO: combine shared whitelists with CAN requests Request( [HYUNDAI_VERSION_REQUEST_LONG], [HYUNDAI_VERSION_RESPONSE], - whitelist_ecus=[Ecu.fwdCamera, Ecu.fwdRadar, Ecu.cornerRadar], - bus=4, + whitelist_ecus=[Ecu.fwdCamera, Ecu.fwdRadar, Ecu.cornerRadar, Ecu.hvac], + bus=0, + auxiliary=True, ), Request( [HYUNDAI_VERSION_REQUEST_LONG], [HYUNDAI_VERSION_RESPONSE], - whitelist_ecus=[Ecu.fwdCamera, Ecu.adas, Ecu.cornerRadar], - bus=5, + whitelist_ecus=[Ecu.fwdCamera, Ecu.adas, Ecu.cornerRadar, Ecu.hvac], + bus=1, + auxiliary=True, + obd_multiplexing=False, + ), + + # CAN-FD debugging queries + Request( + [HYUNDAI_VERSION_REQUEST_ALT], + [HYUNDAI_VERSION_RESPONSE], + whitelist_ecus=[Ecu.parkingAdas, Ecu.hvac], + bus=0, + auxiliary=True, + ), + Request( + [HYUNDAI_VERSION_REQUEST_ALT], + [HYUNDAI_VERSION_RESPONSE], + whitelist_ecus=[Ecu.parkingAdas, Ecu.hvac], + bus=1, + auxiliary=True, + obd_multiplexing=False, ), ], extra_ecus=[ (Ecu.adas, 0x730, None), # ADAS Driving ECU on HDA2 platforms + (Ecu.parkingAdas, 0x7b1, None), # ADAS Parking ECU (may exist on all platforms) + (Ecu.hvac, 0x7b3, None), # HVAC Control Assembly (Ecu.cornerRadar, 0x7b7, None), ], ) @@ -424,6 +454,7 @@ FW_VERSIONS = { b'\xf1\x00AEhe SCC F-CUP 1.00 1.00 99110-G2200 ', b'\xf1\x00AEhe SCC F-CUP 1.00 1.00 99110-G2600 ', b'\xf1\x00AEhe SCC F-CUP 1.00 1.02 99110-G2100 ', + b'\xf1\x00AEhe SCC FHCUP 1.00 1.00 99110-G2600 ', ], (Ecu.eps, 0x7d4, None): [ b'\xf1\x00AE MDPS C 1.00 1.01 56310/G2510 4APHC101', @@ -511,7 +542,6 @@ FW_VERSIONS = { b'\xf1\x00DN8_ SCC FHCUP 1.00 1.00 99110-L0000 ', b'\xf1\x00DN8_ SCC FHCUP 1.00 1.01 99110-L1000 ', b'\xf1\x00DN89110-L0000 \xaa\xaa\xaa\xaa\xaa\xaa\xaa ', - b'\xf1\x8799110L0000\xf1\x00DN8_ SCC F-CUP 1.00 1.00 99110-L0000 ', ], (Ecu.abs, 0x7d1, None): [ b'\xf1\x00DN ESC \x07 106 \x07\x01 58910-L0100', @@ -520,6 +550,7 @@ FW_VERSIONS = { b'\xf1\x00DN ESC \x06 104\x19\x08\x01 58910-L0100', b'\xf1\x00DN ESC \x07 104\x19\x08\x01 58910-L0100', b'\xf1\x00DN ESC \x08 103\x19\x06\x01 58910-L1300', + b'\xf1\x00DN ESC \x07 107"\x08\x07 58910-L0100', b'\xf1\x8758910-L0100\xf1\x00DN ESC \x07 106 \x07\x01 58910-L0100', b'\xf1\x8758910-L0100\xf1\x00DN ESC \x06 104\x19\x08\x01 58910-L0100', b'\xf1\x8758910-L0100\xf1\x00DN ESC \x06 106 \x07\x01 58910-L0100', @@ -543,6 +574,7 @@ FW_VERSIONS = { b'HM6M2_0a0_BD0', b'\xf1\x8739110-2S278\xf1\x82DNDVD5GMCCXXXL5B', b'\xf1\x8739110-2S041\xf1\x81HM6M1_0a0_M00', + b'\xf1\x8739110-2S042\xf1\x81HM6M1_0a0_M00', b'\xf1\x81HM6M1_0a0_G20', ], (Ecu.eps, 0x7d4, None): [ @@ -562,6 +594,7 @@ FW_VERSIONS = { b'\xf1\x8757700-L0000\xf1\x00DN8 MDPS R 1.00 1.00 57700-L0000 4DNAP100', b'\xf1\x00DN8 MDPS R 1.00 1.00 57700-L0000 4DNAP101', b'\xf1\x00DN8 MDPS C 1.00 1.01 56310-L0210 4DNAC102', + b'\xf1\x00DN8 MDPS C 1.00 1.01 56310L0200\x00 4DNAC102', ], (Ecu.fwdCamera, 0x7c4, None): [ b'\xf1\x00DN8 MFC AT KOR LHD 1.00 1.02 99211-L1000 190422', @@ -882,18 +915,23 @@ FW_VERSIONS = { CAR.KIA_STINGER_2022: { (Ecu.fwdRadar, 0x7d0, None): [ b'\xf1\x00CK__ SCC F-CUP 1.00 1.00 99110-J5500 ', + b'\xf1\x00CK__ SCC FHCUP 1.00 1.00 99110-J5500 ', ], (Ecu.engine, 0x7e0, None): [ b'\xf1\x81640R0051\x00\x00\x00\x00\x00\x00\x00\x00', + b'\xf1\x81HM6M1_0a0_H00', ], (Ecu.eps, 0x7d4, None): [ b'\xf1\x00CK MDPS R 1.00 5.03 57700-J5380 4C2VR503', + b'\xf1\x00CK MDPS R 1.00 5.03 57700-J5300 4C2CL503', ], (Ecu.fwdCamera, 0x7c4, None): [ b'\xf1\x00CK MFC AT AUS RHD 1.00 1.00 99211-J5500 210622', + b'\xf1\x00CK MFC AT KOR LHD 1.00 1.00 99211-J5500 210622', ], (Ecu.transmission, 0x7e1, None): [ b'\xf1\x87VCNLF11383972DK1vffV\x99\x99\x89\x98\x86eUU\x88wg\x89vfff\x97fff\x99\x87o\xff"\xc1\xf1\x81E30\x00\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 E30\x00\x00\x00\x00\x00\x00\x00SCK0T33GH0\xbe`\xfb\xc6', + b'\xf1\x00bcsh8p54 E31\x00\x00\x00\x00\x00\x00\x00SCK0T25KH2B\xfbI\xe2', ], }, CAR.PALISADE: { @@ -1025,11 +1063,23 @@ FW_VERSIONS = { ], }, CAR.GENESIS_G70: { - (Ecu.fwdRadar, 0x7d0, None): [b'\xf1\x00IK__ SCC F-CUP 1.00 1.02 96400-G9100 ', ], - (Ecu.engine, 0x7e0, None): [b'\xf1\x81640F0051\x00\x00\x00\x00\x00\x00\x00\x00', ], - (Ecu.eps, 0x7d4, None): [b'\xf1\x00IK MDPS R 1.00 1.06 57700-G9420 4I4VL106', ], - (Ecu.fwdCamera, 0x7c4, None): [b'\xf1\x00IK MFC AT USA LHD 1.00 1.01 95740-G9000 170920', ], - (Ecu.transmission, 0x7e1, None): [b'\xf1\x87VDJLT17895112DN4\x88fVf\x99\x88\x88\x88\x87fVe\x88vhwwUFU\x97eFex\x99\xff\xb7\x82\xf1\x81E25\x00\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 E25\x00\x00\x00\x00\x00\x00\x00SIK0T33NB2\x11\x1am\xda', ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00IK__ SCC F-CUP 1.00 1.02 96400-G9100 ', + b'\xf1\x00IK__ SCC F-CUP 1.00 1.01 96400-G9100 ', + ], + (Ecu.engine, 0x7e0, None): [ + b'\xf1\x81640F0051\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.eps, 0x7d4, None): [ + b'\xf1\x00IK MDPS R 1.00 1.06 57700-G9420 4I4VL106', + ], + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00IK MFC AT USA LHD 1.00 1.01 95740-G9000 170920', + ], + (Ecu.transmission, 0x7e1, None): [ + b'\xf1\x00bcsh8p54 E25\x00\x00\x00\x00\x00\x00\x00SIK0T33NB2\x11\x1am\xda', + b'\xf1\x87VDJLT17895112DN4\x88fVf\x99\x88\x88\x88\x87fVe\x88vhwwUFU\x97eFex\x99\xff\xb7\x82\xf1\x81E25\x00\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 E25\x00\x00\x00\x00\x00\x00\x00SIK0T33NB2\x11\x1am\xda', + ], }, CAR.GENESIS_G70_2020: { (Ecu.eps, 0x7d4, None): [ @@ -1263,6 +1313,14 @@ FW_VERSIONS = { b'\xf1\x00DEE MFC AT KOR LHD 1.00 1.03 95740-Q4000 180821', ], }, + CAR.KIA_NIRO_EV_2ND_GEN: { + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00SG2_ RDR ----- 1.00 1.01 99110-AT000 ', + ], + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00SG2EMFC AT EUR LHD 1.01 1.09 99211-AT000 220801', + ], + }, CAR.KIA_NIRO_PHEV: { (Ecu.engine, 0x7e0, None): [ b'\xf1\x816H6F4051\x00\x00\x00\x00\x00\x00\x00\x00', @@ -1444,25 +1502,29 @@ FW_VERSIONS = { b'\xf1\x00CN7HMFC AT USA LHD 1.00 1.05 99210-AA000 210930', b'\xf1\000CN7HMFC AT USA LHD 1.00 1.03 99210-AA000 200819', b'\xf1\x00CN7HMFC AT USA LHD 1.00 1.07 99210-AA000 220426', + b'\xf1\x00CN7HMFC AT USA LHD 1.00 1.08 99210-AA000 220728', ], (Ecu.fwdRadar, 0x7d0, None): [ - b'\xf1\000CNhe SCC FHCUP 1.00 1.01 99110-BY000 ', + b'\xf1\x00CNhe SCC FHCUP 1.00 1.01 99110-BY000 ', b'\xf1\x8799110BY000\xf1\x00CNhe SCC FHCUP 1.00 1.01 99110-BY000 ', ], (Ecu.eps, 0x7d4, None): [ b'\xf1\x00CN7 MDPS C 1.00 1.03 56310BY0500 4CNHC103', b'\xf1\x8756310/BY050\xf1\x00CN7 MDPS C 1.00 1.03 56310/BY050 4CNHC103', b'\xf1\x8756310/BY050\xf1\000CN7 MDPS C 1.00 1.02 56310/BY050 4CNHC102', + b'\xf1\x00CN7 MDPS C 1.00 1.04 56310BY050\x00 4CNHC104', ], (Ecu.transmission, 0x7e1, None): [ b'\xf1\0006U3L0_C2\000\0006U3K3051\000\000HCN0G16NS0\xb9?A\xaa', b'\xf1\0006U3L0_C2\000\0006U3K3051\000\000HCN0G16NS0\000\000\000\000', b'\xf1\x816U3K3051\000\000\xf1\0006U3L0_C2\000\0006U3K3051\000\000HCN0G16NS0\xb9?A\xaa', b'\xf1\x816U3K3051\x00\x00\xf1\x006U3L0_C2\x00\x006U3K3051\x00\x00HCN0G16NS0\x00\x00\x00\x00', + b'\xf1\x006U3L0_C2\x00\x006U3K9051\x00\x00HCN0G16NS1\x00\x00\x00\x00', ], (Ecu.engine, 0x7e0, None): [ b'\xf1\x816H6G5051\x00\x00\x00\x00\x00\x00\x00\x00', b'\xf1\x816H6G6051\x00\x00\x00\x00\x00\x00\x00\x00', + b'\xf1\x816H6G8051\x00\x00\x00\x00\x00\x00\x00\x00', ] }, CAR.KONA_HEV: { @@ -1553,6 +1615,7 @@ FW_VERSIONS = { b'\xf1\x00CV1 MFC AT EUR RHD 1.00 1.00 99210-CV100 220630', b'\xf1\x00CV1 MFC AT USA LHD 1.00 1.00 99210-CV100 220630', b'\xf1\x00CV1 MFC AT KOR LHD 1.00 1.04 99210-CV000 210823', + b'\xf1\x00CV1 MFC AT KOR LHD 1.00 1.06 99210-CV000 220328', ], }, CAR.IONIQ_5: { @@ -1572,9 +1635,12 @@ FW_VERSIONS = { }, CAR.TUCSON_4TH_GEN: { (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-N9210 14G', b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.01 99211-N9240 14T', + b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-CW010 14X', ], (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00NX4__ 1.00 1.00 99110-N9100 ', b'\xf1\x00NX4__ 1.01 1.00 99110-N9100 ', ], }, @@ -1590,6 +1656,7 @@ FW_VERSIONS = { }, CAR.KIA_SPORTAGE_HYBRID_5TH_GEN: { (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00NQ5 FR_CMR AT GEN LHD 1.00 1.00 99211-P1060 665', b'\xf1\x00NQ5 FR_CMR AT USA LHD 1.00 1.00 99211-P1060 665', ], (Ecu.fwdRadar, 0x7d0, None): [ @@ -1662,14 +1729,14 @@ CHECKSUM = { "6B": [CAR.KIA_SORENTO, CAR.HYUNDAI_GENESIS], } -FEATURES = { +CAN_GEARS = { # which message has the gear "use_cluster_gears": {CAR.ELANTRA, CAR.KONA}, "use_tcu_gears": {CAR.KIA_OPTIMA_G4, CAR.KIA_OPTIMA_G4_FL, CAR.SONATA_LF, CAR.VELOSTER, CAR.TUCSON}, "use_elect_gears": {CAR.KIA_NIRO_EV, CAR.KIA_NIRO_PHEV, CAR.KIA_NIRO_HEV_2021, CAR.KIA_OPTIMA_H, CAR.IONIQ_EV_LTD, CAR.KONA_EV, CAR.IONIQ, CAR.IONIQ_EV_2020, CAR.IONIQ_PHEV, CAR.ELANTRA_HEV_2021, CAR.SONATA_HYBRID, CAR.KONA_HEV, CAR.IONIQ_HEV_2022, CAR.SANTA_FE_HEV_2022, CAR.SANTA_FE_PHEV_2022, CAR.IONIQ_PHEV_2019, CAR.KONA_EV_2022, CAR.KIA_K5_HEV_2020}, } -CANFD_CAR = {CAR.KIA_EV6, CAR.IONIQ_5, CAR.TUCSON_4TH_GEN, CAR.TUCSON_HYBRID_4TH_GEN, CAR.KIA_SPORTAGE_HYBRID_5TH_GEN, CAR.SANTA_CRUZ_1ST_GEN, CAR.KIA_SPORTAGE_5TH_GEN, CAR.GENESIS_GV70_1ST_GEN, CAR.KIA_SORENTO_PHEV_4TH_GEN, CAR.GENESIS_GV60_EV_1ST_GEN, CAR.KIA_SORENTO_4TH_GEN, CAR.KIA_NIRO_HEV_2ND_GEN} +CANFD_CAR = {CAR.KIA_EV6, CAR.IONIQ_5, CAR.TUCSON_4TH_GEN, CAR.TUCSON_HYBRID_4TH_GEN, CAR.KIA_SPORTAGE_HYBRID_5TH_GEN, CAR.SANTA_CRUZ_1ST_GEN, CAR.KIA_SPORTAGE_5TH_GEN, CAR.GENESIS_GV70_1ST_GEN, CAR.KIA_SORENTO_PHEV_4TH_GEN, CAR.GENESIS_GV60_EV_1ST_GEN, CAR.KIA_SORENTO_4TH_GEN, CAR.KIA_NIRO_HEV_2ND_GEN, CAR.KIA_NIRO_EV_2ND_GEN} # The radar does SCC on these cars when HDA I, rather than the camera CANFD_RADAR_SCC_CAR = {CAR.GENESIS_GV70_1ST_GEN, CAR.KIA_SORENTO_PHEV_4TH_GEN, CAR.KIA_SORENTO_4TH_GEN} @@ -1678,7 +1745,7 @@ CANFD_RADAR_SCC_CAR = {CAR.GENESIS_GV70_1ST_GEN, CAR.KIA_SORENTO_PHEV_4TH_GEN, C CAMERA_SCC_CAR = {CAR.KONA_EV_2022, } HYBRID_CAR = {CAR.IONIQ_PHEV, CAR.ELANTRA_HEV_2021, CAR.KIA_NIRO_PHEV, CAR.KIA_NIRO_HEV_2021, CAR.SONATA_HYBRID, CAR.KONA_HEV, CAR.IONIQ, CAR.IONIQ_HEV_2022, CAR.SANTA_FE_HEV_2022, CAR.SANTA_FE_PHEV_2022, CAR.IONIQ_PHEV_2019, CAR.TUCSON_HYBRID_4TH_GEN, CAR.KIA_SPORTAGE_HYBRID_5TH_GEN, CAR.KIA_SORENTO_PHEV_4TH_GEN, CAR.KIA_K5_HEV_2020, CAR.KIA_NIRO_HEV_2ND_GEN} # these cars use a different gas signal -EV_CAR = {CAR.IONIQ_EV_2020, CAR.IONIQ_EV_LTD, CAR.KONA_EV, CAR.KIA_NIRO_EV, CAR.KONA_EV_2022, CAR.KIA_EV6, CAR.IONIQ_5, CAR.GENESIS_GV60_EV_1ST_GEN} +EV_CAR = {CAR.IONIQ_EV_2020, CAR.IONIQ_EV_LTD, CAR.KONA_EV, CAR.KIA_NIRO_EV, CAR.KIA_NIRO_EV_2ND_GEN, CAR.KONA_EV_2022, CAR.KIA_EV6, CAR.IONIQ_5, CAR.GENESIS_GV60_EV_1ST_GEN} # these cars require a special panda safety mode due to missing counters and checksums in the messages LEGACY_SAFETY_MODE_CAR = {CAR.HYUNDAI_GENESIS, CAR.IONIQ_EV_2020, CAR.IONIQ_EV_LTD, CAR.IONIQ_PHEV, CAR.IONIQ, CAR.KONA_EV, CAR.KIA_SORENTO, CAR.SONATA_LF, CAR.KIA_OPTIMA_G4, CAR.KIA_OPTIMA_G4_FL, CAR.VELOSTER, @@ -1741,4 +1808,5 @@ DBC = { CAR.GENESIS_GV60_EV_1ST_GEN: dbc_dict('hyundai_canfd', None), CAR.KIA_SORENTO_4TH_GEN: dbc_dict('hyundai_canfd', None), CAR.KIA_NIRO_HEV_2ND_GEN: dbc_dict('hyundai_canfd', None), + CAR.KIA_NIRO_EV_2ND_GEN: dbc_dict('hyundai_canfd', None), } diff --git a/selfdrive/car/interfaces.py b/selfdrive/car/interfaces.py index 249818369c..e5d7430878 100644 --- a/selfdrive/car/interfaces.py +++ b/selfdrive/car/interfaces.py @@ -8,10 +8,10 @@ from cereal import car from common.basedir import BASEDIR from common.conversions import Conversions as CV from common.kalman.simple_kalman import KF1D -from common.numpy_fast import clip, interp +from common.numpy_fast import clip from common.realtime import DT_CTRL from selfdrive.car import apply_hysteresis, gen_empty_fingerprint, scale_rot_inertia, scale_tire_stiffness -from selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX, apply_center_deadzone +from selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX, get_friction from selfdrive.controls.lib.events import Events from selfdrive.controls.lib.vehicle_model import VehicleModel @@ -64,6 +64,7 @@ class CarInterfaceBase(ABC): self.frame = 0 self.steering_unpressed = 0 self.low_speed_alert = False + self.no_steer_warning = False self.silent_steer_warning = True self.v_ego_cluster_seen = False @@ -92,12 +93,12 @@ class CarInterfaceBase(ABC): """ Parameters essential to controlling the car may be incomplete or wrong without FW versions or fingerprints. """ - return cls.get_params(candidate, gen_empty_fingerprint(), list(), False) + return cls.get_params(candidate, gen_empty_fingerprint(), list(), False, False) @classmethod - def get_params(cls, candidate: str, fingerprint: Dict[int, Dict[int, int]], car_fw: List[car.CarParams.CarFw], experimental_long: bool): + def get_params(cls, candidate: str, fingerprint: Dict[int, Dict[int, int]], car_fw: List[car.CarParams.CarFw], experimental_long: bool, docs: bool): ret = CarInterfaceBase.get_std_params(candidate) - ret = cls._get_params(ret, candidate, fingerprint, car_fw, experimental_long) + ret = cls._get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs) # Set common params using fields set by the car interface # TODO: get actual value, for now starting with reasonable value for @@ -114,7 +115,7 @@ class CarInterfaceBase(ABC): @staticmethod @abstractmethod - def _get_params(ret: car.CarParams, candidate: str, fingerprint: Dict[int, Dict[int, int]], car_fw: List[car.CarParams.CarFw], experimental_long: bool): + def _get_params(ret: car.CarParams, candidate: str, fingerprint: Dict[int, Dict[int, int]], car_fw: List[car.CarParams.CarFw], experimental_long: bool, docs: bool): raise NotImplementedError @staticmethod @@ -131,15 +132,11 @@ class CarInterfaceBase(ABC): return self.get_steer_feedforward_default @staticmethod - def torque_from_lateral_accel_linear(lateral_accel_value, torque_params, lateral_accel_error, lateral_accel_deadzone, friction_compensation): + def torque_from_lateral_accel_linear(lateral_accel_value: float, torque_params: car.CarParams.LateralTorqueTuning, + lateral_accel_error: float, lateral_accel_deadzone: float, friction_compensation: bool) -> float: # The default is a linear relationship between torque and lateral acceleration (accounting for road roll and steering friction) - friction_interp = interp( - apply_center_deadzone(lateral_accel_error, lateral_accel_deadzone), - [-FRICTION_THRESHOLD, FRICTION_THRESHOLD], - [-torque_params.friction, torque_params.friction] - ) - friction = friction_interp if friction_compensation else 0.0 - return (lateral_accel_value / torque_params.latAccelFactor) + friction + friction = get_friction(lateral_accel_error, lateral_accel_deadzone, FRICTION_THRESHOLD, torque_params, friction_compensation) + return (lateral_accel_value / float(torque_params.latAccelFactor)) + friction def torque_from_lateral_accel(self) -> TorqueFromLateralAccelCallbackType: return self.torque_from_lateral_accel_linear @@ -282,13 +279,19 @@ class CarInterfaceBase(ABC): # Handle permanent and temporary steering faults self.steering_unpressed = 0 if cs_out.steeringPressed else self.steering_unpressed + 1 if cs_out.steerFaultTemporary: - # if the user overrode recently, show a less harsh alert - if self.silent_steer_warning or cs_out.standstill or self.steering_unpressed < int(1.5 / DT_CTRL): - self.silent_steer_warning = True - events.add(EventName.steerTempUnavailableSilent) + if cs_out.steeringPressed and (not self.CS.out.steerFaultTemporary or self.no_steer_warning): + self.no_steer_warning = True else: - events.add(EventName.steerTempUnavailable) + self.no_steer_warning = False + + # if the user overrode recently, show a less harsh alert + if self.silent_steer_warning or cs_out.standstill or self.steering_unpressed < int(1.5 / DT_CTRL): + self.silent_steer_warning = True + events.add(EventName.steerTempUnavailableSilent) + else: + events.add(EventName.steerTempUnavailable) else: + self.no_steer_warning = False self.silent_steer_warning = False if cs_out.steerFaultPermanent: events.add(EventName.steerUnavailable) diff --git a/selfdrive/car/isotp_parallel_query.py b/selfdrive/car/isotp_parallel_query.py index d9c658a14c..965d2e1836 100644 --- a/selfdrive/car/isotp_parallel_query.py +++ b/selfdrive/car/isotp_parallel_query.py @@ -60,7 +60,7 @@ class IsoTpParallelQuery: return msgs def _drain_rx(self): - messaging.drain_sock(self.logcan) + messaging.drain_sock_raw(self.logcan) self.msg_buffer = defaultdict(list) def _create_isotp_msg(self, tx_addr, sub_addr, rx_addr): @@ -100,18 +100,16 @@ class IsoTpParallelQuery: while True: self.rx() - if all(request_done.values()): - break - for tx_addr, msg in msgs.items(): try: - dat, updated = msg.recv() + dat, rx_in_progress = msg.recv() except Exception: cloudlog.exception(f"Error processing UDS response: {tx_addr}") request_done[tx_addr] = True continue - if updated: + # Extend timeout for each consecutive ISO-TP frame to avoid timing out on long responses + if rx_in_progress: response_timeouts[tx_addr] = time.monotonic() + timeout if not dat: @@ -123,6 +121,7 @@ class IsoTpParallelQuery: if response_valid: if counter + 1 < len(self.request): + response_timeouts[tx_addr] = time.monotonic() + timeout msg.send(self.request[counter + 1]) request_counter[tx_addr] += 1 else: @@ -132,18 +131,21 @@ class IsoTpParallelQuery: error_code = dat[2] if len(dat) > 2 else -1 if error_code == 0x78: response_timeouts[tx_addr] = time.monotonic() + self.response_pending_timeout - if self.debug: - cloudlog.warning(f"iso-tp query response pending: {tx_addr}") + cloudlog.error(f"iso-tp query response pending: {tx_addr}") else: - response_timeouts[tx_addr] = 0 request_done[tx_addr] = True cloudlog.error(f"iso-tp query bad response: {tx_addr} - 0x{dat.hex()}") + # Mark request done if address timed out cur_time = time.monotonic() - if cur_time - max(response_timeouts.values()) > 0: - for tx_addr in msgs: + for tx_addr in response_timeouts: + if cur_time - response_timeouts[tx_addr] > 0: if request_counter[tx_addr] > 0 and not request_done[tx_addr]: cloudlog.error(f"iso-tp query timeout after receiving response: {tx_addr}") + request_done[tx_addr] = True + + # Break if all requests are done (finished or timed out) + if all(request_done.values()): break if cur_time - start_time > total_timeout: diff --git a/selfdrive/car/mazda/interface.py b/selfdrive/car/mazda/interface.py index 2930b002d4..443116bc18 100755 --- a/selfdrive/car/mazda/interface.py +++ b/selfdrive/car/mazda/interface.py @@ -11,7 +11,7 @@ EventName = car.CarEvent.EventName class CarInterface(CarInterfaceBase): @staticmethod - def _get_params(ret, candidate, fingerprint, car_fw, experimental_long): + def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs): ret.carName = "mazda" ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.mazda)] ret.radarUnavailable = True diff --git a/selfdrive/car/mazda/values.py b/selfdrive/car/mazda/values.py index 598b598a16..8f993e2651 100644 --- a/selfdrive/car/mazda/values.py +++ b/selfdrive/car/mazda/values.py @@ -258,11 +258,14 @@ FW_VERSIONS = { CAR.MAZDA6: { (Ecu.eps, 0x730, None): [ b'GBEF-3210X-B-00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'GBEF-3210X-C-00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'GFBC-3210X-A-00\000\000\000\000\000\000\000\000\000', ], (Ecu.engine, 0x7e0, None): [ + b'PA34-188K2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PX4F-188K2-D\000\000\000\000\000\000\000\000\000\000\000\000', b'PYH7-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'PYH7-188K2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x764, None): [ b'K131-67XK2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', @@ -270,13 +273,16 @@ FW_VERSIONS = { ], (Ecu.abs, 0x760, None): [ b'GBVH-437K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'GBVH-437K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'GDDM-437K2-A\000\000\000\000\000\000\000\000\000\000\000\000', ], (Ecu.fwdCamera, 0x706, None): [ b'B61L-67XK2-S\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'B61L-67XK2-T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'GSH7-67XK2-P\000\000\000\000\000\000\000\000\000\000\000\000', ], (Ecu.transmission, 0x7e1, None): [ + b'PA28-21PS1-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PYH3-21PS1-D\000\000\000\000\000\000\000\000\000\000\000\000', b'PYH7-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], @@ -291,6 +297,7 @@ FW_VERSIONS = { b'PXM4-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PXM4-188K2-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PXM6-188K2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'PXGW-188K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x764, None): [ b'K131-67XK2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', diff --git a/selfdrive/car/mock/interface.py b/selfdrive/car/mock/interface.py index 13210c86d5..1c74aef1fa 100755 --- a/selfdrive/car/mock/interface.py +++ b/selfdrive/car/mock/interface.py @@ -19,7 +19,7 @@ class CarInterface(CarInterfaceBase): self.prev_speed = 0. @staticmethod - def _get_params(ret, candidate, fingerprint, car_fw, experimental_long): + def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs): ret.carName = "mock" ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.noOutput)] ret.mass = 1700. diff --git a/selfdrive/car/nissan/interface.py b/selfdrive/car/nissan/interface.py index 074cd1cc57..573dff9f05 100644 --- a/selfdrive/car/nissan/interface.py +++ b/selfdrive/car/nissan/interface.py @@ -8,7 +8,7 @@ from selfdrive.car.nissan.values import CAR class CarInterface(CarInterfaceBase): @staticmethod - def _get_params(ret, candidate, fingerprint, car_fw, experimental_long): + def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs): ret.carName = "nissan" ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.nissan)] ret.autoResumeSng = False diff --git a/selfdrive/car/nissan/values.py b/selfdrive/car/nissan/values.py index e9af828e2b..d0f0aa93c9 100644 --- a/selfdrive/car/nissan/values.py +++ b/selfdrive/car/nissan/values.py @@ -39,7 +39,7 @@ class NissanCarInfo(CarInfo): CAR_INFO: Dict[str, Optional[Union[NissanCarInfo, List[NissanCarInfo]]]] = { CAR.XTRAIL: NissanCarInfo("Nissan X-Trail 2017"), - CAR.LEAF: NissanCarInfo("Nissan Leaf 2018-22", video_link="https://youtu.be/vaMbtAh_0cY"), + CAR.LEAF: NissanCarInfo("Nissan Leaf 2018-23", video_link="https://youtu.be/vaMbtAh_0cY"), CAR.LEAF_IC: None, # same platforms CAR.ROGUE: NissanCarInfo("Nissan Rogue 2018-20"), CAR.ALTIMA: NissanCarInfo("Nissan Altima 2019-20", harness=Harness.nissan_b), @@ -126,18 +126,22 @@ FW_VERSIONS = { (Ecu.fwdCamera, 0x707, None): [ b'5SH1BDB\x04\x18\x00\x00\x00\x00\x00_-?\x04\x91\xf2\x00\x00\x00\x80', b'5SK0ADB\x04\x18\x00\x00\x00\x00\x00_(5\x07\x9aQ\x00\x00\x00\x80', + b'5SH4BDB\x04\x18\x00\x00\x00\x00\x00_-?\x04\x91\xf2\x00\x00\x00\x80', ], (Ecu.abs, 0x740, None): [ b'476605SH1D', b'476605SK2A', + b'476605SD2E', ], (Ecu.eps, 0x742, None): [ b'5SH2A\x99A\x05\x02N123F\x15\x81\x00\x00\x00\x00\x00\x00\x00\x80', b'5SK3A\x99A\x05\x02N123F\x15u\x00\x00\x00\x00\x00\x00\x00\x80', + b'5SH2C\xb7A\x05\x02N123F\x15\xa3\x00\x00\x00\x00\x00\x00\x00\x80', ], (Ecu.gateway, 0x18dad0f1, None): [ b'284U25SH3A', b'284U25SK2D', + b'284U25SF0C', ], }, CAR.XTRAIL: { diff --git a/selfdrive/car/subaru/carcontroller.py b/selfdrive/car/subaru/carcontroller.py index f508c80e08..ecc28ddd16 100644 --- a/selfdrive/car/subaru/carcontroller.py +++ b/selfdrive/car/subaru/carcontroller.py @@ -2,7 +2,7 @@ from common.numpy_fast import clip from opendbc.can.packer import CANPacker from selfdrive.car import apply_driver_steer_torque_limits from selfdrive.car.subaru import subarucan -from selfdrive.car.subaru.values import DBC, GLOBAL_GEN2, PREGLOBAL_CARS, CarControllerParams +from selfdrive.car.subaru.values import DBC, GLOBAL_GEN2, PREGLOBAL_CARS, CarControllerParams, SubaruFlags ACCEL_HYST_GAP = 10 # don't change accel command for small oscilalitons within this value @@ -33,6 +33,7 @@ class CarController: self.brake_status_cnt = -1 self.es_status_cnt = -1 self.es_brake_cnt = -1 + self.infotainmentstatus_cnt = -1 self.cruise_button_prev = 0 self.steer_rate_limited = False self.cruise_rpm_last = 0 @@ -136,8 +137,8 @@ class CarController: if self.es_lkas_state_cnt != CS.es_lkas_state_msg["COUNTER"]: can_sends.append(subarucan.create_es_lkas_state(self.packer, CS.es_lkas_state_msg, CC.enabled, hud_control.visualAlert, - hud_control.leftLaneVisible, hud_control.rightLaneVisible, - hud_control.leftLaneDepart, hud_control.rightLaneDepart)) + hud_control.leftLaneVisible, hud_control.rightLaneVisible, + hud_control.leftLaneDepart, hud_control.rightLaneDepart)) self.es_lkas_state_cnt = CS.es_lkas_state_msg["COUNTER"] if self.CP.openpilotLongitudinalControl: @@ -167,6 +168,10 @@ class CarController: can_sends.append(subarucan.create_es_distance(self.packer, CS.es_distance_msg, bus, pcm_cancel_cmd, CC.longActive, brake_cmd, brake_value, cruise_throttle)) self.last_cancel_frame = self.frame + if self.CP.flags & SubaruFlags.SEND_INFOTAINMENT and self.infotainmentstatus_cnt != CS.es_infotainmentstatus_msg["COUNTER"]: + can_sends.append(subarucan.create_infotainmentstatus(self.packer, CS.es_infotainmentstatus_msg, hud_control.visualAlert)) + self.infotainmentstatus_cnt = CS.es_infotainmentstatus_msg["COUNTER"] + new_actuators = actuators.copy() new_actuators.steer = self.apply_steer_last / self.p.STEER_MAX new_actuators.steerOutputCan = self.apply_steer_last diff --git a/selfdrive/car/subaru/carstate.py b/selfdrive/car/subaru/carstate.py index 4c7d308c22..b06de84384 100644 --- a/selfdrive/car/subaru/carstate.py +++ b/selfdrive/car/subaru/carstate.py @@ -4,7 +4,7 @@ from opendbc.can.can_define import CANDefine from common.conversions import Conversions as CV from selfdrive.car.interfaces import CarStateBase from opendbc.can.parser import CANParser -from selfdrive.car.subaru.values import DBC, CAR, GLOBAL_GEN2, PREGLOBAL_CARS +from selfdrive.car.subaru.values import DBC, CAR, GLOBAL_GEN2, PREGLOBAL_CARS, SubaruFlags class CarState(CarStateBase): @@ -90,6 +90,8 @@ class CarState(CarStateBase): cp_es_distance = cp_body if self.car_fingerprint in GLOBAL_GEN2 else cp_cam self.es_distance_msg = copy.copy(cp_es_distance.vl["ES_Distance"]) self.es_dashstatus_msg = copy.copy(cp_cam.vl["ES_DashStatus"]) + if self.CP.flags & SubaruFlags.SEND_INFOTAINMENT: + self.es_infotainmentstatus_msg = copy.copy(cp_cam.vl["INFOTAINMENT_STATUS"]) return ret @@ -343,6 +345,15 @@ class CarState(CarStateBase): signals += CarState.get_global_es_signals()[0] checks += CarState.get_global_es_signals()[1] + if CP.flags & SubaruFlags.SEND_INFOTAINMENT: + signals += [ + ("LKAS_State_Infotainment", "INFOTAINMENT_STATUS"), + ("LKAS_Blue_Lines", "INFOTAINMENT_STATUS"), + ("Signal1", "INFOTAINMENT_STATUS"), + ("Signal2", "INFOTAINMENT_STATUS"), + ] + checks.append(("INFOTAINMENT_STATUS", 10)) + return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, 2) @staticmethod diff --git a/selfdrive/car/subaru/interface.py b/selfdrive/car/subaru/interface.py index 9519053e98..21db225a1d 100644 --- a/selfdrive/car/subaru/interface.py +++ b/selfdrive/car/subaru/interface.py @@ -3,18 +3,22 @@ from cereal import car from panda import Panda from selfdrive.car import STD_CARGO_KG, get_safety_config from selfdrive.car.interfaces import CarInterfaceBase -from selfdrive.car.subaru.values import CAR, GLOBAL_GEN2, PREGLOBAL_CARS +from selfdrive.car.subaru.values import CAR, GLOBAL_GEN2, PREGLOBAL_CARS, SubaruFlags class CarInterface(CarInterfaceBase): @staticmethod - def _get_params(ret, candidate, fingerprint, car_fw, experimental_long): + def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs): ret.carName = "subaru" ret.radarUnavailable = True ret.dashcamOnly = candidate in PREGLOBAL_CARS ret.autoResumeSng = False + # Detect infotainment message sent from the camera + if candidate not in PREGLOBAL_CARS and 0x323 in fingerprint[2]: + ret.flags |= SubaruFlags.SEND_INFOTAINMENT.value + if candidate in PREGLOBAL_CARS: ret.enableBsm = 0x25c in fingerprint[0] ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.subaruLegacy)] diff --git a/selfdrive/car/subaru/subarucan.py b/selfdrive/car/subaru/subarucan.py index 1d47a06bd7..637d7ba5bf 100644 --- a/selfdrive/car/subaru/subarucan.py +++ b/selfdrive/car/subaru/subarucan.py @@ -65,11 +65,8 @@ def create_es_lkas_state(packer, es_lkas_state_msg, enabled, visual_alert, left_ elif right_lane_depart: values["LKAS_Alert"] = 11 # Right lane departure dash alert - if enabled: - values["LKAS_ACTIVE"] = 1 # Show LKAS lane lines - values["LKAS_Dash_State"] = 2 # Green enabled indicator - else: - values["LKAS_Dash_State"] = 0 # LKAS Not enabled + values["LKAS_ACTIVE"] = 1 # Show LKAS lane lines + values["LKAS_Dash_State"] = 2 if enabled else 0 # Green enabled indicator values["LKAS_Left_Line_Visible"] = int(left_line) values["LKAS_Right_Line_Visible"] = int(right_line) @@ -86,7 +83,7 @@ def create_es_dashstatus(packer, es_dashstatus_msg, enabled, long_active, lead_v values["Car_Follow"] = int(lead_visible) # Filter stock LKAS disabled and Keep hands on steering wheel OFF alerts - if values["LKAS_State_Msg"] in [2, 3]: + if values["LKAS_State_Msg"] in (2, 3): values["LKAS_State_Msg"] = 0 return packer.make_can_msg("ES_DashStatus", 0, values) @@ -129,6 +126,21 @@ def create_brake_status(packer, brake_status_msg, aeb): return packer.make_can_msg("Brake_Status", 2, values) +def create_infotainmentstatus(packer, infotainmentstatus_msg, visual_alert): + # Filter stock LKAS disabled and Keep hands on steering wheel OFF alerts + if infotainmentstatus_msg["LKAS_State_Infotainment"] in (3, 4): + infotainmentstatus_msg["LKAS_State_Infotainment"] = 0 + + # Show Keep hands on wheel alert for openpilot steerRequired alert + if visual_alert == VisualAlert.steerRequired: + infotainmentstatus_msg["LKAS_State_Infotainment"] = 3 + + # Show Obstacle Detected for fcw + if visual_alert == VisualAlert.fcw: + infotainmentstatus_msg["LKAS_State_Infotainment"] = 2 + + return packer.make_can_msg("INFOTAINMENT_STATUS", 0, infotainmentstatus_msg) + # *** Subaru Pre-global *** def subaru_preglobal_checksum(packer, values, addr): diff --git a/selfdrive/car/subaru/values.py b/selfdrive/car/subaru/values.py index ae140d95c6..a7cb949326 100644 --- a/selfdrive/car/subaru/values.py +++ b/selfdrive/car/subaru/values.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from enum import Enum +from enum import Enum, IntFlag from typing import Dict, List, Union from cereal import car @@ -45,6 +45,10 @@ class CarControllerParams: BRAKE_MAX = 400 BRAKE_SCALE = 1000 # from testing +class SubaruFlags(IntFlag): + SEND_INFOTAINMENT = 1 + + class CAR: # Global platform ASCENT = "SUBARU ASCENT LIMITED 2019" @@ -200,6 +204,7 @@ FW_VERSIONS = { b'\x00\x00d\xdc\x00\x00\x00\x00', b'\x00\x00dd\x00\x00\x00\x00', b'\x00\x00c\xf4\x1f@ \x07', + b'\x00\x00e\x1c\x00\x00\x00\x00', ], (Ecu.engine, 0x7e0, None): [ b'\xaa\x61\x66\x73\x07', @@ -220,6 +225,8 @@ FW_VERSIONS = { b'\xc5!dr\x07', b'\xaa!aw\x07', b'\xaa!av\x07', + b'\xaa\x01bt\x07', + b'\xc5!ap\x07', ], (Ecu.transmission, 0x7e1, None): [ b'\xe3\xe5\x46\x31\x00', @@ -235,6 +242,7 @@ FW_VERSIONS = { b'\xe4\xf5\002\000\000', b'\xe3\xd0\x081\x00', b'\xe3\xf5\x06\x00\x00', + b'\xe3\xd5\x161\x00', ], }, CAR.IMPREZA_2020: { @@ -289,6 +297,7 @@ FW_VERSIONS = { b'\xa3 \x14\x01', b'\xf1\x00\xbb\r\x05', b'\xa3 \x18&\x00', + b'\xa3 \x19&\x00', ], (Ecu.eps, 0x746, None): [ b'\x8d\xc0\x04\x00', @@ -301,6 +310,8 @@ FW_VERSIONS = { b'\xf1\x00\xac\x02\x00', b'\x00\x00e!\x00\x00\x00\x00', b'\x00\x00e\x97\x00\x00\x00\x00', + b'\x00\x00e^\x1f@ !', + b'\x00\x00e^\x00\x00\x00\x00', ], (Ecu.engine, 0x7e0, None): [ b'\xb6"`A\x07', @@ -318,6 +329,7 @@ FW_VERSIONS = { b'\x1a\xf6B`\x00', b'\x1a\xf6b0\x00', b'\x1a\xe6B1\x00', + b'\x1a\xe6F1\x00', ], }, CAR.FORESTER_PREGLOBAL: { @@ -485,6 +497,7 @@ FW_VERSIONS = { b'\xa1 "\t\x01', b'\xa1 \x08\x02', b'\xa1 \x06\x02', + b'\xa1 \x07\x02', b'\xa1 \x08\x00', b'\xa1 "\t\x00', ], @@ -496,6 +509,7 @@ FW_VERSIONS = { (Ecu.fwdCamera, 0x787, None): [ b'\x00\x00eJ\x00\x1f@ \x19\x00', b'\000\000e\x80\000\037@ \031\000', + b'\x00\x00e\x9a\x00\x00\x00\x00\x00\x00', b'\x00\x00e\x9a\x00\x1f@ 1\x00', b'\x00\x00eJ\x00\x00\x00\x00\x00\x00', ], @@ -505,6 +519,7 @@ FW_VERSIONS = { b'\xde"`0\a', b'\xf1\x82\xbc,\xa0q\a', b'\xf1\x82\xe3,\xa0@\x07', + b'\xe2"`0\x07', b'\xe2"`p\x07', b'\xf1\x82\xe2,\xa0@\x07', b'\xbc"`q\x07', @@ -516,6 +531,7 @@ FW_VERSIONS = { b'\xa5\xfe\xf6@\x00', b'\xa7\x8e\xf40\x00', b'\xf1\x82\xa7\xf6D@\x00', + b'\xa7\xf6D@\x00', b'\xa7\xfe\xf4@\x00', ], }, diff --git a/selfdrive/car/tesla/interface.py b/selfdrive/car/tesla/interface.py index 70d49896cb..afd3fb3be4 100755 --- a/selfdrive/car/tesla/interface.py +++ b/selfdrive/car/tesla/interface.py @@ -8,7 +8,7 @@ from selfdrive.car.interfaces import CarInterfaceBase class CarInterface(CarInterfaceBase): @staticmethod - def _get_params(ret, candidate, fingerprint, car_fw, experimental_long): + def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs): ret.carName = "tesla" # There is no safe way to do steer blending with user torque, diff --git a/selfdrive/car/tests/routes.py b/selfdrive/car/tests/routes.py index 4dd19d24e3..3935461e61 100644 --- a/selfdrive/car/tests/routes.py +++ b/selfdrive/car/tests/routes.py @@ -59,6 +59,7 @@ routes = [ CarTestRoute("f08912a233c1584f|2022-08-11--18-02-41", GM.BOLT_EUV, segment=1), CarTestRoute("555d4087cf86aa91|2022-12-02--12-15-07", GM.BOLT_EUV, segment=14), # Bolt EV CarTestRoute("38aa7da107d5d252|2022-08-15--16-01-12", GM.SILVERADO), + CarTestRoute("5085c761395d1fe6|2023-04-07--18-20-06", GM.TRAILBLAZER), CarTestRoute("0e7a2ba168465df5|2020-10-18--14-14-22", HONDA.ACURA_RDX_3G), CarTestRoute("a74b011b32b51b56|2020-07-26--17-09-36", HONDA.CIVIC), @@ -68,6 +69,7 @@ routes = [ CarTestRoute("52f3e9ae60c0d886|2021-05-23--15-59-43", HONDA.FIT), CarTestRoute("2c4292a5cd10536c|2021-08-19--21-32-15", HONDA.FREED), CarTestRoute("03be5f2fd5c508d1|2020-04-19--18-44-15", HONDA.HRV), + CarTestRoute("320098ff6c5e4730|2023-04-13--17-47-46", HONDA.HRV_3G), CarTestRoute("917b074700869333|2021-05-24--20-40-20", HONDA.ACURA_ILX), CarTestRoute("08a3deb07573f157|2020-03-06--16-11-19", HONDA.ACCORD), # 1.5T CarTestRoute("1da5847ac2488106|2021-05-24--19-31-50", HONDA.ACCORD), # 2.0T @@ -126,9 +128,11 @@ routes = [ CarTestRoute("d624b3d19adce635|2020-08-01--14-59-12", HYUNDAI.VELOSTER), CarTestRoute("d545129f3ca90f28|2022-10-19--09-22-54", HYUNDAI.KIA_EV6), # HDA2 CarTestRoute("68d6a96e703c00c9|2022-09-10--16-09-39", HYUNDAI.KIA_EV6), # HDA1 + CarTestRoute("9b25e8c1484a1b67|2023-04-13--10-41-45", HYUNDAI.KIA_EV6), CarTestRoute("007d5e4ad9f86d13|2021-09-30--15-09-23", HYUNDAI.KIA_K5_2021), CarTestRoute("c58dfc9fc16590e0|2023-01-14--13-51-48", HYUNDAI.KIA_K5_HEV_2020), CarTestRoute("50c6c9b85fd1ff03|2020-10-26--17-56-06", HYUNDAI.KIA_NIRO_EV), + CarTestRoute("b153671049a867b3|2023-04-05--10-00-30", HYUNDAI.KIA_NIRO_EV_2ND_GEN), CarTestRoute("173219cf50acdd7b|2021-07-05--10-27-41", HYUNDAI.KIA_NIRO_PHEV), CarTestRoute("34a875f29f69841a|2021-07-29--13-02-09", HYUNDAI.KIA_NIRO_HEV_2021), CarTestRoute("db04d2c63990e3ba|2023-02-08--16-52-39", HYUNDAI.KIA_NIRO_HEV_2ND_GEN), @@ -162,6 +166,7 @@ routes = [ CarTestRoute("a5c341bb250ca2f0|2022-05-18--16-05-17", TOYOTA.RAV4_TSS2_2022), CarTestRoute("7e34a988419b5307|2019-12-18--19-13-30", TOYOTA.RAV4H_TSS2), CarTestRoute("2475fb3eb2ffcc2e|2022-04-29--12-46-23", TOYOTA.RAV4H_TSS2_2022), + CarTestRoute("7a31f030957b9c85|2023-04-01--14-12-51", TOYOTA.LEXUS_ES), CarTestRoute("e6a24be49a6cd46e|2019-10-29--10-52-42", TOYOTA.LEXUS_ES_TSS2), CarTestRoute("da23c367491f53e2|2021-05-21--09-09-11", TOYOTA.LEXUS_CTH, segment=3), CarTestRoute("f49e8041283f2939|2019-05-30--11-51-51", TOYOTA.LEXUS_ESH_TSS2), @@ -185,6 +190,7 @@ routes = [ CarTestRoute("9b36accae406390e|2021-03-30--10-41-38", TOYOTA.MIRAI), CarTestRoute("cd9cff4b0b26c435|2021-05-13--15-12-39", TOYOTA.CHR), CarTestRoute("ea8fbe72b96a185c|2023-02-08--15-11-46", TOYOTA.CHR_TSS2), + CarTestRoute("ea8fbe72b96a185c|2023-02-22--09-20-34", TOYOTA.CHR_TSS2), # openpilot longitudinal, with smartDSU CarTestRoute("57858ede0369a261|2021-05-18--20-34-20", TOYOTA.CHRH), CarTestRoute("6719965b0e1d1737|2023-02-09--22-44-05", TOYOTA.CHRH_TSS2), CarTestRoute("14623aae37e549f3|2021-10-24--01-20-49", TOYOTA.PRIUS_V), diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index ac8213e4c1..8e48722482 100755 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -25,7 +25,7 @@ class TestCarInterfaces(unittest.TestCase): car_fw = [] - car_params = CarInterface.get_params(car_name, fingerprints, car_fw, experimental_long=False) + car_params = CarInterface.get_params(car_name, fingerprints, car_fw, experimental_long=False, docs=False) car_interface = CarInterface(car_params, CarController, CarState) assert car_params assert car_interface diff --git a/selfdrive/car/tests/test_fw_fingerprint.py b/selfdrive/car/tests/test_fw_fingerprint.py index e9f23145cd..c069280d11 100755 --- a/selfdrive/car/tests/test_fw_fingerprint.py +++ b/selfdrive/car/tests/test_fw_fingerprint.py @@ -1,19 +1,29 @@ #!/usr/bin/env python3 import random +import time import unittest from collections import defaultdict from parameterized import parameterized +import threading from cereal import car -from selfdrive.car.car_helpers import get_interface_attr, interfaces +from common.params import Params +from selfdrive.car.car_helpers import interfaces from selfdrive.car.fingerprints import FW_VERSIONS -from selfdrive.car.fw_versions import FW_QUERY_CONFIGS, match_fw_to_car +from selfdrive.car.fw_versions import FW_QUERY_CONFIGS, VERSIONS, match_fw_to_car, get_fw_versions CarFw = car.CarParams.CarFw Ecu = car.CarParams.Ecu ECU_NAME = {v: k for k, v in Ecu.schema.enumerants.items()} -VERSIONS = get_interface_attr("FW_VERSIONS", ignore_none=True) + + +class FakeSocket: + def receive(self, non_blocking=False): + pass + + def send(self, msg): + pass class TestFwFingerprint(unittest.TestCase): @@ -104,5 +114,69 @@ class TestFwFingerprint(unittest.TestCase): f'{brand.title()}: FW query whitelist missing ecus: {ecu_strings}') +class TestFwFingerprintTiming(unittest.TestCase): + @staticmethod + def _benchmark(brand, num_pandas, n): + params = Params() + fake_socket = FakeSocket() + + times = [] + for _ in range(n): + params.put_bool("ObdMultiplexingEnabled", True) + thread = threading.Thread(target=get_fw_versions, args=(fake_socket, fake_socket, brand), kwargs=dict(num_pandas=num_pandas)) + thread.start() + t = time.perf_counter() + while thread.is_alive(): + time.sleep(0.02) + if not params.get_bool("ObdMultiplexingChanged"): + params.put_bool("ObdMultiplexingChanged", True) + times.append(time.perf_counter() - t) + + return round(sum(times) / len(times), 2) + + def _assert_timing(self, avg_time, ref_time, tol): + self.assertLess(avg_time, ref_time + tol) + self.assertGreater(avg_time, ref_time - tol, "Performance seems to have improved, update test refs.") + + def test_fw_query_timing(self): + tol = 0.1 + total_ref_time = 4.6 + brand_ref_times = { + 1: { + 'body': 0.1, + 'chrysler': 0.3, + 'ford': 0.2, + 'honda': 0.5, + 'hyundai': 0.7, + 'mazda': 0.1, + 'nissan': 0.3, + 'subaru': 0.1, + 'tesla': 0.2, + 'toyota': 0.7, + 'volkswagen': 0.2, + }, + 2: { + 'hyundai': 1.1, + } + } + + total_time = 0 + for num_pandas in (1, 2): + for brand, config in FW_QUERY_CONFIGS.items(): + with self.subTest(brand=brand, num_pandas=num_pandas): + multi_panda_requests = [r for r in config.requests if r.bus > 3] + if not len(multi_panda_requests) and num_pandas > 1: + raise unittest.SkipTest("No multi-panda FW queries") + + avg_time = self._benchmark(brand, num_pandas, 10) + total_time += avg_time + self._assert_timing(avg_time, brand_ref_times[num_pandas][brand], tol) + print(f'{brand=}, {num_pandas=}, {len(config.requests)=}, avg FW query time={avg_time} seconds') + + with self.subTest(brand='all_brands'): + self._assert_timing(total_time, total_ref_time, tol) + print(f'all brands, total FW query time={total_time} seconds') + + if __name__ == "__main__": unittest.main() diff --git a/selfdrive/car/tests/test_lateral_limits.py b/selfdrive/car/tests/test_lateral_limits.py index 3efdd7404e..7ccd5e3c97 100755 --- a/selfdrive/car/tests/test_lateral_limits.py +++ b/selfdrive/car/tests/test_lateral_limits.py @@ -10,24 +10,23 @@ from common.realtime import DT_CTRL from selfdrive.car.car_helpers import interfaces from selfdrive.car.fingerprints import all_known_cars from selfdrive.car.interfaces import get_torque_params -from selfdrive.car.hyundai.values import CAR as HYUNDAI +from selfdrive.car.subaru.values import CAR as SUBARU CAR_MODELS = all_known_cars() -# ISO 11270 -MAX_LAT_JERK = 2.5 # m/s^3 -MAX_LAT_JERK_TOLERANCE = 0.75 # m/s^3 -MAX_LAT_ACCEL = 3.0 # m/s^2 +# ISO 11270 - allowed up jerk is strictly lower than recommended limits +MAX_LAT_ACCEL = 3.0 # m/s^2 +MAX_LAT_JERK_UP = 2.5 # m/s^3 +MAX_LAT_JERK_DOWN = 5.0 # m/s^3 +MAX_LAT_JERK_UP_TOLERANCE = 0.5 # m/s^3 # jerk is measured over half a second -JERK_MEAS_FRAMES = 0.5 / DT_CTRL +JERK_MEAS_T = 0.5 -# TODO: update the max measured lateral accel for these cars +# TODO: put these cars within limits ABOVE_LIMITS_CARS = [ - HYUNDAI.KONA_EV, - HYUNDAI.KONA_HEV, - HYUNDAI.KONA, - HYUNDAI.KONA_EV_2022, + SUBARU.LEGACY, + SUBARU.OUTBACK, ] car_model_jerks: DefaultDict[str, Dict[str, float]] = defaultdict(dict) @@ -62,20 +61,24 @@ class TestLateralLimits(unittest.TestCase): @staticmethod def calculate_0_5s_jerk(control_params, torque_params): steer_step = control_params.STEER_STEP - steer_up_per_frame = (control_params.STEER_DELTA_UP / control_params.STEER_MAX) / steer_step - steer_down_per_frame = (control_params.STEER_DELTA_DOWN / control_params.STEER_MAX) / steer_step + max_lat_accel = torque_params['MAX_LAT_ACCEL_MEASURED'] - steer_up_0_5_sec = min(steer_up_per_frame * JERK_MEAS_FRAMES, 1.0) - steer_down_0_5_sec = min(steer_down_per_frame * JERK_MEAS_FRAMES, 1.0) + # Steer up/down delta per 10ms frame, in percentage of max torque + steer_up_per_frame = control_params.STEER_DELTA_UP / control_params.STEER_MAX / steer_step + steer_down_per_frame = control_params.STEER_DELTA_DOWN / control_params.STEER_MAX / steer_step - max_lat_accel = torque_params['MAX_LAT_ACCEL_MEASURED'] - return steer_up_0_5_sec * max_lat_accel, steer_down_0_5_sec * max_lat_accel + # Lateral acceleration reached in 0.5 seconds, clipping to max torque + accel_up_0_5_sec = min(steer_up_per_frame * JERK_MEAS_T / DT_CTRL, 1.0) * max_lat_accel + accel_down_0_5_sec = min(steer_down_per_frame * JERK_MEAS_T / DT_CTRL, 1.0) * max_lat_accel + + # Convert to m/s^3 + return accel_up_0_5_sec / JERK_MEAS_T, accel_down_0_5_sec / JERK_MEAS_T def test_jerk_limits(self): up_jerk, down_jerk = self.calculate_0_5s_jerk(self.control_params, self.torque_params) car_model_jerks[self.car_model] = {"up_jerk": up_jerk, "down_jerk": down_jerk} - self.assertLessEqual(up_jerk, MAX_LAT_JERK + MAX_LAT_JERK_TOLERANCE) - self.assertLessEqual(down_jerk, MAX_LAT_JERK + MAX_LAT_JERK_TOLERANCE) + self.assertLessEqual(up_jerk, MAX_LAT_JERK_UP + MAX_LAT_JERK_UP_TOLERANCE) + self.assertLessEqual(down_jerk, MAX_LAT_JERK_DOWN) def test_max_lateral_accel(self): self.assertLessEqual(self.torque_params["MAX_LAT_ACCEL_MEASURED"], MAX_LAT_ACCEL) @@ -88,7 +91,8 @@ if __name__ == "__main__": max_car_model_len = max([len(car_model) for car_model in car_model_jerks]) for car_model, _jerks in sorted(car_model_jerks.items(), key=lambda i: i[1]['up_jerk'], reverse=True): - violation = any([_jerk >= MAX_LAT_JERK + MAX_LAT_JERK_TOLERANCE for _jerk in _jerks.values()]) + violation = _jerks["up_jerk"] > MAX_LAT_JERK_UP + MAX_LAT_JERK_UP_TOLERANCE or \ + _jerks["down_jerk"] > MAX_LAT_JERK_DOWN violation_str = " - VIOLATION" if violation else "" print(f"{car_model:{max_car_model_len}} - up jerk: {round(_jerks['up_jerk'], 2):5} m/s^3, down jerk: {round(_jerks['down_jerk'], 2):5} m/s^3{violation_str}") diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py index 6fbe1436f1..6a89da3e7c 100755 --- a/selfdrive/car/tests/test_models.py +++ b/selfdrive/car/tests/test_models.py @@ -103,7 +103,7 @@ class TestCarModelBase(unittest.TestCase): cls.can_msgs = sorted(can_msgs, key=lambda msg: msg.logMonoTime) cls.CarInterface, cls.CarController, cls.CarState = interfaces[cls.car_model] - cls.CP = cls.CarInterface.get_params(cls.car_model, fingerprint, car_fw, experimental_long) + cls.CP = cls.CarInterface.get_params(cls.car_model, fingerprint, car_fw, experimental_long, docs=False) assert cls.CP assert cls.CP.carFingerprint == cls.car_model diff --git a/selfdrive/car/torque_data/override.yaml b/selfdrive/car/torque_data/override.yaml index 75051316fa..2aa049a89d 100644 --- a/selfdrive/car/torque_data/override.yaml +++ b/selfdrive/car/torque_data/override.yaml @@ -33,6 +33,7 @@ SUBARU OUTBACK 6TH GEN: [2.3, 2.3, 0.11] CADILLAC ESCALADE 2017: [1.899999976158142, 1.842270016670227, 0.1120000034570694] CHEVROLET BOLT EUV 2022: [2.0, 2.0, 0.05] CHEVROLET SILVERADO 1500 2020: [1.9, 1.9, 0.112] +CHEVROLET TRAILBLAZER 2021: [1.33, 1.9, 0.16] CHEVROLET EQUINOX 2019: [2.0, 2.0, 0.05] VOLKSWAGEN PASSAT NMS: [2.5, 2.5, 0.1] VOLKSWAGEN SHARAN 2ND GEN: [2.5, 2.5, 0.1] @@ -44,9 +45,11 @@ KIA SORENTO PLUG-IN HYBRID 4TH GEN: [2.5, 2.5, 0.1] GENESIS GV60 ELECTRIC 1ST GEN: [2.5, 2.5, 0.1] KIA SORENTO 4TH GEN: [2.5, 2.5, 0.1] KIA NIRO HYBRID 2ND GEN: [2.42, 2.5, 0.12] +KIA NIRO EV 2ND GEN: [2.05, 2.5, 0.14] # Dashcam or fallback configured as ideal car mock: [10.0, 10, 0.0] # Manually checked HONDA CIVIC 2022: [2.5, 1.2, 0.15] +HONDA HR-V 2023: [2.5, 1.2, 0.2] diff --git a/selfdrive/car/torque_data/params.yaml b/selfdrive/car/torque_data/params.yaml index 6f8cfe0ce6..cb423e1d87 100644 --- a/selfdrive/car/torque_data/params.yaml +++ b/selfdrive/car/torque_data/params.yaml @@ -30,7 +30,7 @@ HYUNDAI IONIQ 5 2022: [3.172929, 2.713050, 0.096019] HYUNDAI IONIQ ELECTRIC LIMITED 2019: [1.7662975472852054, 1.613755614526594, 0.17087579756306276] HYUNDAI IONIQ PHEV 2020: [3.2928700076638537, 2.1193482926455656, 0.12463700961468778] HYUNDAI IONIQ PLUG-IN HYBRID 2019: [2.970807902012267, 1.6312321830002083, 0.1088964990357482] -HYUNDAI KONA ELECTRIC 2019: [3.078814714619148, 3.2961956260770484, 0.12359762054065548] +HYUNDAI KONA ELECTRIC 2019: [3.078814714619148, 2.307336938253934, 0.12359762054065548] HYUNDAI PALISADE 2020: [2.544642494803999, 1.8721703683337008, 0.1301424599248651] HYUNDAI SANTA FE 2019: [3.0787027729757632, 2.6173437483495565, 0.1207019341823945] HYUNDAI SANTA FE HYBRID 2022: [3.501877602644835, 2.729064118456137, 0.10384068104538963] diff --git a/selfdrive/car/torque_data/substitute.yaml b/selfdrive/car/torque_data/substitute.yaml index 5feef12206..5ea84e5591 100644 --- a/selfdrive/car/torque_data/substitute.yaml +++ b/selfdrive/car/torque_data/substitute.yaml @@ -12,6 +12,7 @@ TOYOTA C-HR HYBRID 2018: TOYOTA C-HR 2018 TOYOTA C-HR HYBRID 2022: TOYOTA C-HR 2021 LEXUS IS 2018: LEXUS NX 2018 LEXUS CT HYBRID 2018 : LEXUS NX 2018 +LEXUS ES 2018: TOYOTA CAMRY HYBRID 2018 LEXUS ES HYBRID 2018: TOYOTA CAMRY HYBRID 2018 LEXUS NX HYBRID 2020: LEXUS NX 2020 LEXUS RC 2020: LEXUS NX 2020 diff --git a/selfdrive/car/toyota/carstate.py b/selfdrive/car/toyota/carstate.py index 050f8747a2..68adc2ee57 100644 --- a/selfdrive/car/toyota/carstate.py +++ b/selfdrive/car/toyota/carstate.py @@ -115,7 +115,8 @@ class CarState(CarStateBase): cp_acc = cp_cam if self.CP.carFingerprint in (TSS2_CAR - RADAR_ACC_CAR) else cp if self.CP.carFingerprint in (TSS2_CAR | RADAR_ACC_CAR): - self.acc_type = cp_acc.vl["ACC_CONTROL"]["ACC_TYPE"] + if not (self.CP.flags & ToyotaFlags.SMART_DSU.value): + self.acc_type = cp_acc.vl["ACC_CONTROL"]["ACC_TYPE"] ret.stockFcw = bool(cp_acc.vl["ACC_HUD"]["FCW"]) # some TSS2 cars have low speed lockout permanently set, so ignore on those cars @@ -235,12 +236,17 @@ class CarState(CarStateBase): checks.append(("BSM", 1)) if CP.carFingerprint in RADAR_ACC_CAR: + if not CP.flags & ToyotaFlags.SMART_DSU.value: + signals += [ + ("ACC_TYPE", "ACC_CONTROL"), + ] + checks += [ + ("ACC_CONTROL", 33), + ] signals += [ - ("ACC_TYPE", "ACC_CONTROL"), ("FCW", "ACC_HUD"), ] checks += [ - ("ACC_CONTROL", 33), ("ACC_HUD", 1), ] diff --git a/selfdrive/car/toyota/interface.py b/selfdrive/car/toyota/interface.py index e6476863c3..c222363d18 100644 --- a/selfdrive/car/toyota/interface.py +++ b/selfdrive/car/toyota/interface.py @@ -2,7 +2,7 @@ from cereal import car from common.conversions import Conversions as CV from panda import Panda -from selfdrive.car.toyota.values import Ecu, CAR, ToyotaFlags, CarControllerParams, TSS2_CAR, RADAR_ACC_CAR, NO_DSU_CAR, \ +from selfdrive.car.toyota.values import Ecu, CAR, DBC, ToyotaFlags, CarControllerParams, TSS2_CAR, RADAR_ACC_CAR, NO_DSU_CAR, \ MIN_ACC_SPEED, EPS_SCALE, EV_HYBRID_CAR, UNSUPPORTED_DSU_CAR, NO_STOP_TIMER_CAR, ANGLE_CONTROL_CAR from selfdrive.car import STD_CARGO_KG, scale_tire_stiffness, get_safety_config from selfdrive.car.interfaces import CarInterfaceBase @@ -16,12 +16,13 @@ class CarInterface(CarInterfaceBase): return CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX @staticmethod - def _get_params(ret, candidate, fingerprint, car_fw, experimental_long): + def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs): ret.carName = "toyota" ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.toyota)] ret.safetyConfigs[0].safetyParam = EPS_SCALE[candidate] - if candidate in (CAR.RAV4, CAR.PRIUS_V, CAR.COROLLA, CAR.LEXUS_ESH, CAR.LEXUS_CTH): + # BRAKE_MODULE is on a different address for these cars + if DBC[candidate]["pt"] == "toyota_new_mc_pt_generated": ret.safetyConfigs[0].safetyParam |= Panda.FLAG_TOYOTA_ALT_BRAKE if candidate in ANGLE_CONTROL_CAR: @@ -137,8 +138,9 @@ class CarInterface(CarInterfaceBase): tire_stiffness_factor = 0.444 # not optimized yet ret.mass = 3060. * CV.LB_TO_KG + STD_CARGO_KG - elif candidate in (CAR.LEXUS_ES_TSS2, CAR.LEXUS_ESH_TSS2, CAR.LEXUS_ESH): - stop_and_go = True + elif candidate in (CAR.LEXUS_ES, CAR.LEXUS_ESH, CAR.LEXUS_ES_TSS2, CAR.LEXUS_ESH_TSS2): + if candidate not in (CAR.LEXUS_ES,): # TODO: LEXUS_ES may have sng + stop_and_go = True ret.wheelbase = 2.8702 ret.steerRatio = 16.0 # not optimized tire_stiffness_factor = 0.444 # not optimized yet @@ -200,14 +202,18 @@ class CarInterface(CarInterfaceBase): tire_stiffness_factor=tire_stiffness_factor) ret.enableBsm = 0x3F6 in fingerprint[0] and candidate in TSS2_CAR - # Detect smartDSU, which intercepts ACC_CMD from the DSU allowing openpilot to send it - smartDsu = 0x2FF in fingerprint[0] - # In TSS2 cars the camera does long control + + # Detect smartDSU, which intercepts ACC_CMD from the DSU (or radar) allowing openpilot to send it + if 0x2FF in fingerprint[0]: + ret.flags |= ToyotaFlags.SMART_DSU.value + + # In TSS2 cars, the camera does long control found_ecus = [fw.ecu for fw in car_fw] - ret.enableDsu = len(found_ecus) > 0 and Ecu.dsu not in found_ecus and candidate not in (NO_DSU_CAR | UNSUPPORTED_DSU_CAR) and not smartDsu + ret.enableDsu = len(found_ecus) > 0 and Ecu.dsu not in found_ecus and candidate not in (NO_DSU_CAR | UNSUPPORTED_DSU_CAR) and not (ret.flags & ToyotaFlags.SMART_DSU) ret.enableGasInterceptor = 0x201 in fingerprint[0] + # if the smartDSU is detected, openpilot can send ACC_CMD (and the smartDSU will block it from the DSU) or not (the DSU is "connected") - ret.openpilotLongitudinalControl = smartDsu or ret.enableDsu or candidate in (TSS2_CAR - RADAR_ACC_CAR) + ret.openpilotLongitudinalControl = bool(ret.flags & ToyotaFlags.SMART_DSU) or ret.enableDsu or candidate in (TSS2_CAR - RADAR_ACC_CAR) ret.autoResumeSng = ret.openpilotLongitudinalControl and candidate in NO_STOP_TIMER_CAR if not ret.openpilotLongitudinalControl: diff --git a/selfdrive/car/toyota/toyotacan.py b/selfdrive/car/toyota/toyotacan.py index 7e360cc4e1..a78b70608c 100644 --- a/selfdrive/car/toyota/toyotacan.py +++ b/selfdrive/car/toyota/toyotacan.py @@ -17,7 +17,7 @@ def create_lta_steer_command(packer, steer, steer_req, raw_cnt): "SETME_X1": 1, "SETME_X3": 3, "PERCENTAGE": 100, - "SETME_X64": 0x64, + "SETME_X64": 0, "ANGLE": 0, "STEER_ANGLE_CMD": steer, "STEER_REQUEST": steer_req, diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 4edafc0c28..2fe60e8fb6 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -33,6 +33,7 @@ class CarControllerParams: class ToyotaFlags(IntFlag): HYBRID = 1 + SMART_DSU = 2 class CAR: @@ -76,6 +77,7 @@ class CAR: # Lexus LEXUS_CTH = "LEXUS CT HYBRID 2018" + LEXUS_ES = "LEXUS ES 2018" LEXUS_ESH = "LEXUS ES HYBRID 2018" LEXUS_ES_TSS2 = "LEXUS ES 2019" LEXUS_ESH_TSS2 = "LEXUS ES HYBRID 2019" @@ -140,7 +142,7 @@ CAR_INFO: Dict[str, Union[ToyotaCarInfo, List[ToyotaCarInfo]]] = { CAR.HIGHLANDERH: ToyotaCarInfo("Toyota Highlander Hybrid 2017-19"), CAR.HIGHLANDERH_TSS2: ToyotaCarInfo("Toyota Highlander Hybrid 2020-23"), CAR.PRIUS: [ - ToyotaCarInfo("Toyota Prius 2016", "Toyota Safety Sense P", "https://www.youtube.com/watch?v=8zopPJI8XQ0"), + ToyotaCarInfo("Toyota Prius 2016", "Toyota Safety Sense P", video_link="https://www.youtube.com/watch?v=8zopPJI8XQ0"), ToyotaCarInfo("Toyota Prius 2017-20", video_link="https://www.youtube.com/watch?v=8zopPJI8XQ0"), ToyotaCarInfo("Toyota Prius Prime 2017-20", video_link="https://www.youtube.com/watch?v=8zopPJI8XQ0"), ], @@ -154,7 +156,7 @@ CAR_INFO: Dict[str, Union[ToyotaCarInfo, List[ToyotaCarInfo]]] = { ToyotaCarInfo("Toyota RAV4 2017-18") ], CAR.RAV4H: [ - ToyotaCarInfo("Toyota RAV4 Hybrid 2016", "Toyota Safety Sense P", "https://youtu.be/LhT5VzJVfNI?t=26"), + ToyotaCarInfo("Toyota RAV4 Hybrid 2016", "Toyota Safety Sense P", video_link="https://youtu.be/LhT5VzJVfNI?t=26"), ToyotaCarInfo("Toyota RAV4 Hybrid 2017-18", video_link="https://youtu.be/LhT5VzJVfNI?t=26") ], CAR.RAV4_TSS2: ToyotaCarInfo("Toyota RAV4 2019-21", video_link="https://www.youtube.com/watch?v=wJxjDd42gGA"), @@ -168,7 +170,8 @@ CAR_INFO: Dict[str, Union[ToyotaCarInfo, List[ToyotaCarInfo]]] = { # Lexus CAR.LEXUS_CTH: ToyotaCarInfo("Lexus CT Hybrid 2017-18", "Lexus Safety System+"), - CAR.LEXUS_ESH: ToyotaCarInfo("Lexus ES Hybrid 2017-18", "Lexus Safety System+"), + CAR.LEXUS_ES: ToyotaCarInfo("Lexus ES 2017-18"), + CAR.LEXUS_ESH: ToyotaCarInfo("Lexus ES Hybrid 2017-18"), CAR.LEXUS_ES_TSS2: ToyotaCarInfo("Lexus ES 2019-22"), CAR.LEXUS_ESH_TSS2: ToyotaCarInfo("Lexus ES Hybrid 2019-23", video_link="https://youtu.be/BZ29osRVJeg?t=12"), CAR.LEXUS_IS: ToyotaCarInfo("Lexus IS 2017-19"), @@ -176,7 +179,7 @@ CAR_INFO: Dict[str, Union[ToyotaCarInfo, List[ToyotaCarInfo]]] = { CAR.LEXUS_NXH: ToyotaCarInfo("Lexus NX Hybrid 2018-19"), CAR.LEXUS_NX_TSS2: ToyotaCarInfo("Lexus NX 2020-21"), CAR.LEXUS_NXH_TSS2: ToyotaCarInfo("Lexus NX Hybrid 2020-21"), - CAR.LEXUS_RC: ToyotaCarInfo("Lexus RC 2017-20"), + CAR.LEXUS_RC: ToyotaCarInfo("Lexus RC 2018-20"), CAR.LEXUS_RX: [ ToyotaCarInfo("Lexus RX 2016", "Lexus Safety System+"), ToyotaCarInfo("Lexus RX 2017-19"), @@ -192,23 +195,24 @@ CAR_INFO: Dict[str, Union[ToyotaCarInfo, List[ToyotaCarInfo]]] = { # (addr, cars, bus, 1/freq*100, vl) STATIC_DSU_MSGS = [ (0x128, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.LEXUS_NX, CAR.RAV4, CAR.COROLLA, CAR.AVALON), 1, 3, b'\xf4\x01\x90\x83\x00\x37'), - (0x128, (CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ESH), 1, 3, b'\x03\x00\x20\x00\x00\x52'), - (0x141, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.LEXUS_NX, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ESH, CAR.LEXUS_RX, CAR.PRIUS_V), 1, 2, b'\x00\x00\x00\x46'), - (0x160, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.LEXUS_NX, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ESH, CAR.LEXUS_RX, CAR.PRIUS_V), 1, 7, b'\x00\x00\x08\x12\x01\x31\x9c\x51'), - (0x161, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.LEXUS_NX, CAR.RAV4, CAR.COROLLA, CAR.AVALON, CAR.LEXUS_RX, CAR.PRIUS_V), 1, 7, b'\x00\x1e\x00\x00\x00\x80\x07'), + (0x128, (CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ES, CAR.LEXUS_ESH), 1, 3, b'\x03\x00\x20\x00\x00\x52'), + (0x141, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.LEXUS_NX, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ES, CAR.LEXUS_ESH, CAR.LEXUS_RX, CAR.PRIUS_V), 1, 2, b'\x00\x00\x00\x46'), + (0x160, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.LEXUS_NX, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ES, CAR.LEXUS_ESH, CAR.LEXUS_RX, CAR.PRIUS_V), 1, 7, b'\x00\x00\x08\x12\x01\x31\x9c\x51'), + (0x161, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.LEXUS_NX, CAR.RAV4, CAR.COROLLA, CAR.AVALON, CAR.LEXUS_RX, CAR.PRIUS_V, CAR.LEXUS_ES), 1, 7, b'\x00\x1e\x00\x00\x00\x80\x07'), (0X161, (CAR.HIGHLANDERH, CAR.HIGHLANDER, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ESH), 1, 7, b'\x00\x1e\x00\xd4\x00\x00\x5b'), - (0x283, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.LEXUS_NX, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ESH, CAR.LEXUS_RX, CAR.PRIUS_V), 0, 3, b'\x00\x00\x00\x00\x00\x00\x8c'), + (0x283, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.LEXUS_NX, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ES, CAR.LEXUS_ESH, CAR.LEXUS_RX, CAR.PRIUS_V), 0, 3, b'\x00\x00\x00\x00\x00\x00\x8c'), (0x2E6, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH), 0, 3, b'\xff\xf8\x00\x08\x7f\xe0\x00\x4e'), (0x2E7, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH), 0, 3, b'\xa8\x9c\x31\x9c\x00\x00\x00\x02'), (0x33E, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH), 0, 20, b'\x0f\xff\x26\x40\x00\x1f\x00'), - (0x344, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.LEXUS_NX, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ESH, CAR.LEXUS_RX, CAR.PRIUS_V), 0, 5, b'\x00\x00\x01\x00\x00\x00\x00\x50'), + (0x344, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.LEXUS_NX, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ES, CAR.LEXUS_ESH, CAR.LEXUS_RX, CAR.PRIUS_V), 0, 5, b'\x00\x00\x01\x00\x00\x00\x00\x50'), (0x365, (CAR.PRIUS, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.LEXUS_NX, CAR.HIGHLANDERH), 0, 20, b'\x00\x00\x00\x80\x03\x00\x08'), - (0x365, (CAR.RAV4, CAR.RAV4H, CAR.COROLLA, CAR.HIGHLANDER, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ESH, CAR.LEXUS_RX, CAR.PRIUS_V), 0, 20, b'\x00\x00\x00\x80\xfc\x00\x08'), + (0x365, (CAR.RAV4, CAR.RAV4H, CAR.COROLLA, CAR.HIGHLANDER, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ES, CAR.LEXUS_ESH, CAR.LEXUS_RX, CAR.PRIUS_V), 0, 20, b'\x00\x00\x00\x80\xfc\x00\x08'), (0x366, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.LEXUS_NX, CAR.HIGHLANDERH), 0, 20, b'\x00\x00\x4d\x82\x40\x02\x00'), (0x366, (CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ESH, CAR.LEXUS_RX, CAR.PRIUS_V), 0, 20, b'\x00\x72\x07\xff\x09\xfe\x00'), + (0x366, (CAR.LEXUS_ES,), 0, 20, b'\x00\x95\x07\xfe\x08\x05\x00'), (0x470, (CAR.PRIUS, CAR.LEXUS_RXH), 1, 100, b'\x00\x00\x02\x7a'), - (0x470, (CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.RAV4H, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ESH, CAR.PRIUS_V), 1, 100, b'\x00\x00\x01\x79'), - (0x4CB, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.LEXUS_NX, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDERH, CAR.HIGHLANDER, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ESH, CAR.LEXUS_RX, CAR.PRIUS_V), 0, 100, b'\x0c\x00\x00\x00\x00\x00\x00\x00'), + (0x470, (CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.RAV4H, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ES, CAR.LEXUS_ESH, CAR.PRIUS_V), 1, 100, b'\x00\x00\x01\x79'), + (0x4CB, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.LEXUS_NX, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDERH, CAR.HIGHLANDER, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ES, CAR.LEXUS_ESH, CAR.LEXUS_RX, CAR.PRIUS_V), 0, 100, b'\x0c\x00\x00\x00\x00\x00\x00\x00'), ] TOYOTA_VERSION_REQUEST = b'\x1a\x88\x01' @@ -327,6 +331,7 @@ FW_VERSIONS = { CAR.AVALON_TSS2: { (Ecu.abs, 0x7b0, None): [ b'\x01F152607240\x00\x00\x00\x00\x00\x00', + b'\x01F152607250\x00\x00\x00\x00\x00\x00', b'\x01F152607280\x00\x00\x00\x00\x00\x00', ], (Ecu.eps, 0x7a1, None): [ @@ -334,6 +339,7 @@ FW_VERSIONS = { ], (Ecu.engine, 0x700, None): [ b'\x01896630742000\x00\x00\x00\x00', + b'\x01896630743000\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x750, 0xf): [ b'\x018821F6201200\x00\x00\x00\x00', @@ -543,6 +549,7 @@ FW_VERSIONS = { b'\x018966306Q5000\x00\x00\x00\x00', b'\x018966306Q9000\x00\x00\x00\x00', b'\x018966306R3000\x00\x00\x00\x00', + b'\x018966306R8000\x00\x00\x00\x00', b'\x018966306T3100\x00\x00\x00\x00', b'\x018966306T3200\x00\x00\x00\x00', b'\x018966306T4000\x00\x00\x00\x00', @@ -555,6 +562,7 @@ FW_VERSIONS = { (Ecu.fwdCamera, 0x750, 0x6d): [ b'\x028646F0602100\x00\x00\x00\x008646G5301200\x00\x00\x00\x00', b'\x028646F0602200\x00\x00\x00\x008646G5301200\x00\x00\x00\x00', + b'\x028646F0602300\x00\x00\x00\x008646G3304000\x00\x00\x00\x00', b'\x028646F3305200\x00\x00\x00\x008646G5301200\x00\x00\x00\x00', b'\x028646F3305200\x00\x00\x00\x008646G3304000\x00\x00\x00\x00', b'\x028646F3305300\x00\x00\x00\x008646G5301200\x00\x00\x00\x00', @@ -727,22 +735,28 @@ FW_VERSIONS = { ], }, CAR.CHRH_TSS2: { - (Ecu.eps, 0x7a1, None): [ - b'8965B10092\x00\x00\x00\x00\x00\x00', - ], - (Ecu.abs, 0x7b0, None): [ - b'F152610041\x00\x00\x00\x00\x00\x00', - ], - (Ecu.engine, 0x700, None): [ - b'\x0189663F438000\x00\x00\x00\x00', - ], - (Ecu.fwdRadar, 0x750, 15): [ - b'\x018821FF410500\x00\x00\x00\x00', - ], - (Ecu.fwdCamera, 0x750, 109): [ - b'\x028646FF413100\x00\x00\x00\x008646GF411100\x00\x00\x00\x00', - ], - }, + (Ecu.eps, 0x7a1, None): [ + b'8965B10092\x00\x00\x00\x00\x00\x00', + b'8965B10091\x00\x00\x00\x00\x00\x00', + b'8965B10111\x00\x00\x00\x00\x00\x00', + ], + (Ecu.abs, 0x7b0, None): [ + b'F152610041\x00\x00\x00\x00\x00\x00', + ], + (Ecu.engine, 0x700, None): [ + b'\x0189663F438000\x00\x00\x00\x00', + b'\x02896631025000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', + b'\x0289663F453000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', + ], + (Ecu.fwdRadar, 0x750, 15): [ + b'\x018821FF410500\x00\x00\x00\x00', + b'\x018821FF410300\x00\x00\x00\x00', + ], + (Ecu.fwdCamera, 0x750, 109): [ + b'\x028646FF413100\x00\x00\x00\x008646GF411100\x00\x00\x00\x00', + b'\x028646FF411100\x00\x00\x00\x008646GF409000\x00\x00\x00\x00', + ], + }, CAR.COROLLA: { (Ecu.engine, 0x7e0, None): [ b'\x0230ZC2000\x00\x00\x00\x00\x00\x00\x00\x0050212000\x00\x00\x00\x00\x00\x00\x00\x00', @@ -910,6 +924,7 @@ FW_VERSIONS = { b'\x028966312K6000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x028966312L0000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x028966312Q3000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', + b'\x028966312Q3100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x028966312Q4000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x038966312L7000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF1205001\x00\x00\x00\x00', b'\x038966312N1000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF1203001\x00\x00\x00\x00', @@ -920,6 +935,7 @@ FW_VERSIONS = { b'8965B12451\x00\x00\x00\x00\x00\x00', b'8965B16011\x00\x00\x00\x00\x00\x00', b'8965B16101\x00\x00\x00\x00\x00\x00', + b'8965B16170\x00\x00\x00\x00\x00\x00', b'8965B76012\x00\x00\x00\x00\x00\x00', b'8965B76050\x00\x00\x00\x00\x00\x00', b'\x018965B12350\x00\x00\x00\x00\x00\x00', @@ -947,6 +963,7 @@ FW_VERSIONS = { b'F152612D00\x00\x00\x00\x00\x00\x00', b'F152616011\x00\x00\x00\x00\x00\x00', b'F152616060\x00\x00\x00\x00\x00\x00', + b'F152616030\x00\x00\x00\x00\x00\x00', b'F152642540\x00\x00\x00\x00\x00\x00', b'F152676293\x00\x00\x00\x00\x00\x00', b'F152676303\x00\x00\x00\x00\x00\x00', @@ -1101,6 +1118,7 @@ FW_VERSIONS = { b'\x01F152648J4000\x00\x00\x00\x00', b'\x01F152648J5000\x00\x00\x00\x00', b'\x01F152648J6000\x00\x00\x00\x00', + b'\x01F15264872700\x00\x00\x00\x00', ], (Ecu.engine, 0x700, None): [ b'\x01896630E67000\x00\x00\x00\x00', @@ -1718,6 +1736,26 @@ FW_VERSIONS = { b'\x028646F3309100\x00\x00\x00\x008646G3304000\x00\x00\x00\x00', ], }, + CAR.LEXUS_ES: { + (Ecu.engine, 0x7e0, None): [ + b'\x02333R0000\x00\x00\x00\x00\x00\x00\x00\x00A0C01000\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.abs, 0x7b0, None): [ + b'F152606202\x00\x00\x00\x00\x00\x00', + ], + (Ecu.dsu, 0x791, None): [ + b'881513309500\x00\x00\x00\x00', + ], + (Ecu.eps, 0x7a1, None): [ + b'8965B33502\x00\x00\x00\x00\x00\x00', + ], + (Ecu.fwdRadar, 0x750, 0xf): [ + b'8821F4701200\x00\x00\x00\x00', + ], + (Ecu.fwdCamera, 0x750, 0x6d): [ + b'8646F3302200\x00\x00\x00\x00', + ], + }, CAR.LEXUS_ESH: { (Ecu.engine, 0x7e0, None): [ b'\x02333M4200\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00', @@ -1985,6 +2023,7 @@ FW_VERSIONS = { b'\x01896634D12100\x00\x00\x00\x00', b'\x01896634D43000\x00\x00\x00\x00', b'\x01896634D44000\x00\x00\x00\x00', + b'\x018966348X0000\x00\x00\x00\x00', ], (Ecu.abs, 0x7b0, None): [ b'\x01F15260E031\x00\x00\x00\x00\x00\x00', @@ -2147,6 +2186,7 @@ DBC = { CAR.RAV4_TSS2_2023: dbc_dict('toyota_nodsu_pt_generated', None), CAR.COROLLA_TSS2: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'), CAR.COROLLAH_TSS2: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'), + CAR.LEXUS_ES: dbc_dict('toyota_new_mc_pt_generated', 'toyota_adas'), CAR.LEXUS_ES_TSS2: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'), CAR.LEXUS_ESH_TSS2: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'), CAR.LEXUS_ESH: dbc_dict('toyota_new_mc_pt_generated', 'toyota_adas'), diff --git a/selfdrive/car/volkswagen/interface.py b/selfdrive/car/volkswagen/interface.py index 2f8bd8661b..874b85e68e 100644 --- a/selfdrive/car/volkswagen/interface.py +++ b/selfdrive/car/volkswagen/interface.py @@ -21,18 +21,16 @@ class CarInterface(CarInterfaceBase): self.cp_ext = self.cp_cam @staticmethod - def _get_params(ret, candidate, fingerprint, car_fw, experimental_long): + def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs): ret.carName = "volkswagen" ret.radarUnavailable = True - use_off_car_defaults = len(fingerprint[0]) == 0 # Pick sensible carParams during offline doc generation/CI jobs - if candidate in PQ_CARS: # Set global PQ35/PQ46/NMS parameters ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.volkswagenPq)] ret.enableBsm = 0x3BA in fingerprint[0] # SWA_1 - if 0x440 in fingerprint[0] or use_off_car_defaults: # Getriebe_1 + if 0x440 in fingerprint[0] or docs: # Getriebe_1 ret.transmissionType = TransmissionType.automatic else: ret.transmissionType = TransmissionType.manual @@ -55,7 +53,7 @@ class CarInterface(CarInterfaceBase): ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.volkswagen)] ret.enableBsm = 0x30F in fingerprint[0] # SWA_01 - if 0xAD in fingerprint[0] or use_off_car_defaults: # Getriebe_11 + if 0xAD in fingerprint[0] or docs: # Getriebe_11 ret.transmissionType = TransmissionType.automatic elif 0x187 in fingerprint[0]: # EV_Gearshift ret.transmissionType = TransmissionType.direct @@ -80,7 +78,7 @@ class CarInterface(CarInterfaceBase): # Global longitudinal tuning defaults, can be overridden per-vehicle - ret.experimentalLongitudinalAvailable = ret.networkLocation == NetworkLocation.gateway or use_off_car_defaults + ret.experimentalLongitudinalAvailable = ret.networkLocation == NetworkLocation.gateway or docs if experimental_long: # Proof-of-concept, prep for E2E only. No radar points available. Panda ALLOW_DEBUG firmware required. ret.openpilotLongitudinalControl = True diff --git a/selfdrive/car/volkswagen/mqbcan.py b/selfdrive/car/volkswagen/mqbcan.py index 30a51f6fe6..b461fd02ae 100644 --- a/selfdrive/car/volkswagen/mqbcan.py +++ b/selfdrive/car/volkswagen/mqbcan.py @@ -1,14 +1,11 @@ def create_steering_control(packer, bus, apply_steer, lkas_enabled): values = { - "SET_ME_0X3": 0x3, - "Assist_Torque": abs(apply_steer), - "Assist_Requested": lkas_enabled, - "Assist_VZ": 1 if apply_steer < 0 else 0, - "HCA_Available": 1, - "HCA_Standby": not lkas_enabled, - "HCA_Active": lkas_enabled, - "SET_ME_0XFE": 0xFE, - "SET_ME_0X07": 0x07, + "HCA_01_Status_HCA": 5 if lkas_enabled else 3, + "HCA_01_LM_Offset": abs(apply_steer), + "HCA_01_LM_OffSign": 1 if apply_steer < 0 else 0, + "HCA_01_Vib_Freq": 18, + "HCA_01_Sendestatus": 1 if lkas_enabled else 0, + "EA_ACC_Wunschgeschwindigkeit": 327.36, } return packer.make_can_msg("HCA_01", bus, values) @@ -56,18 +53,18 @@ def acc_hud_status_value(main_switch_on, acc_faulted, long_active): return acc_control_value(main_switch_on, acc_faulted, long_active) -def create_acc_accel_control(packer, bus, acc_type, enabled, accel, acc_control, stopping, starting, esp_hold): +def create_acc_accel_control(packer, bus, acc_type, acc_enabled, accel, acc_control, stopping, starting, esp_hold): commands = [] acc_06_values = { "ACC_Typ": acc_type, "ACC_Status_ACC": acc_control, - "ACC_StartStopp_Info": enabled, - "ACC_Sollbeschleunigung_02": accel if enabled else 3.01, + "ACC_StartStopp_Info": acc_enabled, + "ACC_Sollbeschleunigung_02": accel if acc_enabled else 3.01, "ACC_zul_Regelabw_unten": 0.2, # TODO: dynamic adjustment of comfort-band "ACC_zul_Regelabw_oben": 0.2, # TODO: dynamic adjustment of comfort-band - "ACC_neg_Sollbeschl_Grad_02": 4.0 if enabled else 0, # TODO: dynamic adjustment of jerk limits - "ACC_pos_Sollbeschl_Grad_02": 4.0 if enabled else 0, # TODO: dynamic adjustment of jerk limits + "ACC_neg_Sollbeschl_Grad_02": 4.0 if acc_enabled else 0, # TODO: dynamic adjustment of jerk limits + "ACC_pos_Sollbeschl_Grad_02": 4.0 if acc_enabled else 0, # TODO: dynamic adjustment of jerk limits "ACC_Anfahren": starting, "ACC_Anhalten": stopping, } @@ -84,9 +81,9 @@ def create_acc_accel_control(packer, bus, acc_type, enabled, accel, acc_control, acc_07_values = { "ACC_Anhalteweg": 0.75 if stopping else 20.46, # Distance to stop (stopping coordinator handles terminal roll-out) - "ACC_Freilauf_Info": 2 if enabled else 0, + "ACC_Freilauf_Info": 2 if acc_enabled else 0, "ACC_Folgebeschl": 3.02, # Not using secondary controller accel unless and until we understand its impact - "ACC_Sollbeschleunigung_02": accel if enabled else 3.01, + "ACC_Sollbeschleunigung_02": accel if acc_enabled else 3.01, "ACC_Anforderung_HMS": acc_hold_type, "ACC_Anfahren": starting, "ACC_Anhalten": stopping, diff --git a/selfdrive/car/volkswagen/pqcan.py b/selfdrive/car/volkswagen/pqcan.py index 84200c2921..bac3ca121d 100644 --- a/selfdrive/car/volkswagen/pqcan.py +++ b/selfdrive/car/volkswagen/pqcan.py @@ -59,17 +59,18 @@ def acc_hud_status_value(main_switch_on, acc_faulted, long_active): return hud_status -def create_acc_accel_control(packer, bus, acc_type, enabled, accel, acc_control, stopping, starting, esp_hold): +def create_acc_accel_control(packer, bus, acc_type, acc_enabled, accel, acc_control, stopping, starting, esp_hold): commands = [] values = { "ACS_Sta_ADR": acc_control, - "ACS_StSt_Info": acc_control != 1, + "ACS_StSt_Info": acc_enabled, "ACS_Typ_ACC": acc_type, "ACS_Anhaltewunsch": acc_type == 1 and stopping, - "ACS_Sollbeschl": accel if acc_control == 1 else 3.01, - "ACS_zul_Regelabw": 0.2 if acc_control == 1 else 1.27, - "ACS_max_AendGrad": 3.0 if acc_control == 1 else 5.08, + "ACS_FreigSollB": acc_enabled, + "ACS_Sollbeschl": accel if acc_enabled else 3.01, + "ACS_zul_Regelabw": 0.2 if acc_enabled else 1.27, + "ACS_max_AendGrad": 3.0 if acc_enabled else 5.08, } commands.append(packer.make_can_msg("ACC_System", bus, values)) @@ -83,9 +84,9 @@ def create_acc_hud_control(packer, bus, acc_hud_status, set_speed, lead_distance "ACA_Zeitluecke": 2, "ACA_V_Wunsch": set_speed, "ACA_gemZeitl": lead_distance, - # TODO: ACA_ID_StaACC, ACA_AnzDisplay, ACA_kmh_mph, ACA_PrioDisp, ACA_Aend_Zeitluecke - # display/display-prio handling probably needed to stop confusing the instrument cluster - # kmh_mph handling probably needed to resolve rounding errors in displayed setpoint + "ACA_PrioDisp": 3, + # TODO: restore dynamic pop-to-foreground/highlight behavior with ACA_PrioDisp and ACA_AnzDisplay + # TODO: ACA_kmh_mph handling probably needed to resolve rounding errors in displayed setpoint } return packer.make_can_msg("ACC_GRA_Anzeige", bus, values) diff --git a/selfdrive/car/volkswagen/values.py b/selfdrive/car/volkswagen/values.py index e63975b0c0..dd2d42797d 100755 --- a/selfdrive/car/volkswagen/values.py +++ b/selfdrive/car/volkswagen/values.py @@ -214,8 +214,8 @@ CAR_INFO: Dict[str, Union[VWCarInfo, List[VWCarInfo]]] = { ], CAR.PASSAT_NMS: VWCarInfo("Volkswagen Passat NMS 2017-22"), CAR.POLO_MK6: [ - VWCarInfo("Volkswagen Polo 2020-22", footnotes=[Footnote.VW_MQB_A0]), - VWCarInfo("Volkswagen Polo GTI 2020-22", footnotes=[Footnote.VW_MQB_A0]), + VWCarInfo("Volkswagen Polo 2018-23", footnotes=[Footnote.VW_MQB_A0]), + VWCarInfo("Volkswagen Polo GTI 2018-23", footnotes=[Footnote.VW_MQB_A0]), ], CAR.SHARAN_MK2: [ VWCarInfo("Volkswagen Sharan 2018-22"), @@ -223,7 +223,10 @@ CAR_INFO: Dict[str, Union[VWCarInfo, List[VWCarInfo]]] = { ], CAR.TAOS_MK1: VWCarInfo("Volkswagen Taos 2022"), CAR.TCROSS_MK1: VWCarInfo("Volkswagen T-Cross 2021", footnotes=[Footnote.VW_MQB_A0]), - CAR.TIGUAN_MK2: VWCarInfo("Volkswagen Tiguan 2018-23"), + CAR.TIGUAN_MK2: [ + VWCarInfo("Volkswagen Tiguan 2018-23"), + VWCarInfo("Volkswagen Tiguan eHybrid 2021-23"), + ], CAR.TOURAN_MK2: VWCarInfo("Volkswagen Touran 2017"), CAR.TRANSPORTER_T61: [ VWCarInfo("Volkswagen Caravelle 2020"), @@ -289,32 +292,39 @@ FW_QUERY_CONFIG = FwQueryConfig( FW_VERSIONS = { CAR.ARTEON_MK1: { (Ecu.engine, 0x7e0, None): [ + b'\xf1\x873G0906259M \xf1\x890003', b'\xf1\x873G0906259F \xf1\x890004', b'\xf1\x873G0906259N \xf1\x890004', b'\xf1\x873G0906259P \xf1\x890001', b'\xf1\x875NA907115H \xf1\x890002', + b'\xf1\x873G0906259G \xf1\x890004', ], (Ecu.transmission, 0x7e1, None): [ + b'\xf1\x870DL300014C \xf1\x893704', b'\xf1\x8709G927158L \xf1\x893611', b'\xf1\x870GC300011L \xf1\x891401', b'\xf1\x870GC300014M \xf1\x892802', b'\xf1\x870GC300040P \xf1\x891401', ], (Ecu.srs, 0x715, None): [ + b'\xf1\x875QF959655AP\xf1\x890755\xf1\x82\x1311110011111311111100110200--1611125F49', b'\xf1\x873Q0959655BK\xf1\x890703\xf1\x82\x0e1616001613121157161111572900', b'\xf1\x873Q0959655BK\xf1\x890703\xf1\x82\x0e1616001613121177161113772900', b'\xf1\x873Q0959655DL\xf1\x890732\xf1\x82\0161812141812171105141123052J00', + b'\xf1\x873Q0959655CK\xf1\x890711\xf1\x82\x0e1712141712141105121122052900', ], (Ecu.eps, 0x712, None): [ + b'\xf1\x875WA907145M \xf1\x891051\xf1\x82\x002NB4202N7N', b'\xf1\x873Q0909144K \xf1\x895072\xf1\x82\x0571B41815A1', b'\xf1\x873Q0909144L \xf1\x895081\xf1\x82\x0571B00817A1', - b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\00567B0020800', + b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567B0020800', b'\xf1\x875WA907145M \xf1\x891051\xf1\x82\x002MB4092M7N', ], (Ecu.fwdRadar, 0x757, None): [ b'\xf1\x872Q0907572AA\xf1\x890396', b'\xf1\x872Q0907572T \xf1\x890383', b'\xf1\x875Q0907572J \xf1\x890654', + b'\xf1\x875Q0907572R \xf1\x890771', ], }, CAR.ATLAS_MK1: { @@ -394,6 +404,7 @@ FW_VERSIONS = { b'\xf1\x8704L906021DT\xf1\x895520', b'\xf1\x8704L906021DT\xf1\x898127', b'\xf1\x8704L906021N \xf1\x895518', + b'\xf1\x8704L906021N \xf1\x898138', b'\xf1\x8704L906026BN\xf1\x891197', b'\xf1\x8704L906026BP\xf1\x897608', b'\xf1\x8704L906026NF\xf1\x899528', @@ -440,6 +451,7 @@ FW_VERSIONS = { b'\xf1\x870CW300045 \xf1\x894531', b'\xf1\x870CW300047D \xf1\x895261', b'\xf1\x870CW300048J \xf1\x890611', + b'\xf1\x870CW300049H \xf1\x890905', b'\xf1\x870D9300012 \xf1\x894904', b'\xf1\x870D9300012 \xf1\x894913', b'\xf1\x870D9300012 \xf1\x894937', @@ -533,6 +545,7 @@ FW_VERSIONS = { (Ecu.fwdRadar, 0x757, None): [ b'\xf1\x875Q0907567G \xf1\x890390\xf1\x82\x0101', b'\xf1\x875Q0907567J \xf1\x890396\xf1\x82\x0101', + b'\xf1\x875Q0907567L \xf1\x890098\xf1\x82\x0101', b'\xf1\x875Q0907572A \xf1\x890141\xf1\x82\x0101', b'\xf1\x875Q0907572B \xf1\x890200\xf1\x82\x0101', b'\xf1\x875Q0907572C \xf1\x890210\xf1\x82\x0101', @@ -669,9 +682,11 @@ FW_VERSIONS = { b'\xf1\x8704C906025H \xf1\x895177', ], (Ecu.transmission, 0x7e1, None): [ + b'\xf1\x870CW300042D \xf1\x891612', b'\xf1\x870CW300050D \xf1\x891908', ], (Ecu.srs, 0x715, None): [ + b'\xf1\x872Q0959655AG\xf1\x890248\xf1\x82\x1218130411110411--04040404231811152H14', b'\xf1\x872Q0959655AJ\xf1\x890250\xf1\x82\x1248130411110416--04040404784811152H14', ], (Ecu.eps, 0x712, None): [ @@ -744,6 +759,7 @@ FW_VERSIONS = { b'\xf1\x8783A907115F \xf1\x890002', b'\xf1\x8783A907115G \xf1\x890001', b'\xf1\x8783A907115K \xf1\x890001', + b'\xf1\x8704E906024AP\xf1\x891461', ], (Ecu.transmission, 0x7e1, None): [ b'\xf1\x8709G927158DT\xf1\x893698', @@ -759,6 +775,7 @@ FW_VERSIONS = { b'\xf1\x870DL300013G \xf1\x892119', b'\xf1\x870DL300013G \xf1\x892120', b'\xf1\x870DL300014C \xf1\x893703', + b'\xf1\x870DD300046K \xf1\x892302', ], (Ecu.srs, 0x715, None): [ b'\xf1\x875Q0959655AR\xf1\x890317\xf1\x82\02331310031333334313132573732379333313100', @@ -770,6 +787,7 @@ FW_VERSIONS = { b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\x1331310031333334313140573752379333423100', b'\xf1\x875Q0959655CB\xf1\x890421\xf1\x82\x1316143231313500314647021750179333613100', b'\xf1\x875Q0959655CG\xf1\x890421\xf1\x82\x1331310031333300314240024050409333613100', + b'\xf1\x875Q0959655CD\xf1\x890421\xf1\x82\x13123112313333003145406F6154619333613100', ], (Ecu.eps, 0x712, None): [ b'\xf1\x875Q0909143M \xf1\x892041\xf1\x820529A6060603', @@ -782,6 +800,7 @@ FW_VERSIONS = { b'\xf1\x875QM909144C \xf1\x891082\xf1\x82\x0521A60604A1', b'\xf1\x875QM909144C \xf1\x891082\xf1\x82\00521A60804A1', b'\xf1\x875QM907144D \xf1\x891063\xf1\x82\x002SA6092SOM', + b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567A6017A00', ], (Ecu.fwdRadar, 0x757, None): [ b'\xf1\x872Q0907572AA\xf1\x890396', @@ -811,18 +830,24 @@ FW_VERSIONS = { }, CAR.TRANSPORTER_T61: { (Ecu.engine, 0x7e0, None): [ + b'\xf1\x8704L906056AG\xf1\x899970', + b'\xf1\x8704L906056AL\xf1\x899970', b'\xf1\x8704L906057AP\xf1\x891186', b'\xf1\x8704L906057N \xf1\x890413', ], (Ecu.transmission, 0x7e1, None): [ b'\xf1\x870BT300012G \xf1\x893102', b'\xf1\x870BT300012E \xf1\x893105', + b'\xf1\x870BT300046R \xf1\x893102', ], (Ecu.srs, 0x715, None): [ - b'\xf1\x872Q0959655AE\xf1\x890506\xf1\x82\02316170411110411--04041704161611152S1411', + b'\xf1\x872Q0959655AE\xf1\x890506\xf1\x82\x1316170411110411--04041704161611152S1411', + b'\xf1\x872Q0959655AE\xf1\x890506\xf1\x82\x1316170411110411--04041704171711152S1411', b'\xf1\x872Q0959655AF\xf1\x890506\xf1\x82\x1316171111110411--04041711121211152S1413', ], (Ecu.eps, 0x712, None): [ + b'\xf1\x877LA909144F \xf1\x897150\xf1\x82\x0532380518A2', + b'\xf1\x877LA909144G \xf1\x897160\xf1\x82\x05333A5519A2', b'\xf1\x877LA909144F \xf1\x897150\xf1\x82\005323A5519A2', ], (Ecu.fwdRadar, 0x757, None): [ @@ -834,9 +859,11 @@ FW_VERSIONS = { b'\xf1\x8705E906018AT\xf1\x899640', ], (Ecu.transmission, 0x7e1, None): [ + b'\xf1\x870CW300050J \xf1\x891911', b'\xf1\x870CW300051M \xf1\x891925', ], (Ecu.srs, 0x715, None): [ + b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\x1311110012333300314240681152119333463100', b'\xf1\x875Q0959655CG\xf1\x890421\xf1\x82\x13111100123333003142404M1152119333613100', ], (Ecu.eps, 0x712, None): [ @@ -863,6 +890,7 @@ FW_VERSIONS = { b'\xf1\x878V0906264B \xf1\x890003', b'\xf1\x878V0907115B \xf1\x890007', b'\xf1\x878V0907404A \xf1\x890005', + b'\xf1\x875G0906259D \xf1\x890002', ], (Ecu.transmission, 0x7e1, None): [ b'\xf1\x870CW300044T \xf1\x895245', @@ -870,6 +898,7 @@ FW_VERSIONS = { b'\xf1\x870D9300012 \xf1\x894912', b'\xf1\x870D9300012 \xf1\x894931', b'\xf1\x870D9300012K \xf1\x894513', + b'\xf1\x870D9300013B \xf1\x894902', b'\xf1\x870D9300013B \xf1\x894931', b'\xf1\x870D9300041N \xf1\x894512', b'\xf1\x870D9300043T \xf1\x899699', @@ -907,11 +936,13 @@ FW_VERSIONS = { b'\xf1\x875Q0909144P \xf1\x891043\xf1\x82\00503G00803A0', b'\xf1\x875Q0909144P \xf1\x891043\xf1\x82\x0503G0G803A0', b'\xf1\x875Q0909144R \xf1\x891061\xf1\x82\00516G00804A1', + b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\x0516G00804A1', b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\00521G00807A1', ], (Ecu.fwdRadar, 0x757, None): [ - b'\xf1\x875Q0907567N \xf1\x890400\xf1\x82\00101', - b'\xf1\x875Q0907572D \xf1\x890304\xf1\x82\00101', + b'\xf1\x875Q0907567N \xf1\x890400\xf1\x82\x0101', + b'\xf1\x875Q0907572F \xf1\x890400\xf1\x82\x0101', + b'\xf1\x875Q0907572D \xf1\x890304\xf1\x82\x0101', b'\xf1\x875Q0907572G \xf1\x890571', b'\xf1\x875Q0907572H \xf1\x890620', b'\xf1\x875Q0907572P \xf1\x890682', @@ -940,14 +971,17 @@ FW_VERSIONS = { b'\xf1\x8705L906022M \xf1\x890901', b'\xf1\x8783A906259 \xf1\x890001', b'\xf1\x8783A906259 \xf1\x890005', + b'\xf1\x8783A906259F \xf1\x890001', ], (Ecu.transmission, 0x7e1, None): [ b'\xf1\x8709G927158CN\xf1\x893608', + b'\xf1\x8709G927158GP\xf1\x893937', b'\xf1\x870GC300045D \xf1\x892802', b'\xf1\x870GC300046F \xf1\x892701', ], (Ecu.srs, 0x715, None): [ b'\xf1\x875Q0959655BF\xf1\x890403\xf1\x82\x1321211111211200311121232152219321422111', + b'\xf1\x875Q0959655BQ\xf1\x890421\xf1\x82\x132121111121120031112124218C219321532111', b'\xf1\x875Q0959655CC\xf1\x890421\xf1\x82\x131111111111120031111224118A119321532111', b'\xf1\x875Q0959655CC\xf1\x890421\xf1\x82\x131111111111120031111237116A119321532111', ], @@ -955,8 +989,10 @@ FW_VERSIONS = { b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567G6000300', b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567G6000800', b'\xf1\x875QF909144B \xf1\x895582\xf1\x82\x0571G60533A1', + b'\xf1\x875TA907145D \xf1\x891051\xf1\x82\x001PG60A1P7N', ], (Ecu.fwdRadar, 0x757, None): [ + b'\xf1\x872Q0907572AA\xf1\x890396', b'\xf1\x872Q0907572R \xf1\x890372', b'\xf1\x872Q0907572T \xf1\x890383', ], @@ -1077,6 +1113,7 @@ FW_VERSIONS = { b'\xf1\x8704L906026HT\xf1\x893617', b'\xf1\x8783A907115E \xf1\x890001', b'\xf1\x8705E906018DJ\xf1\x890915', + b'\xf1\x8705E906018DJ\xf1\x891903', b'\xf1\x875NA907115E \xf1\x890003', b'\xf1\x875NA907115E \xf1\x890005', ], @@ -1179,6 +1216,7 @@ FW_VERSIONS = { b'\xf1\x8704L906026FP\xf1\x891196', b'\xf1\x8704L906026KB\xf1\x894071', b'\xf1\x8704L906026KD\xf1\x894798', + b'\xf1\x8704L906026MT\xf1\x893076', b'\xf1\x873G0906259 \xf1\x890004', b'\xf1\x873G0906259B \xf1\x890002', b'\xf1\x873G0906264A \xf1\x890002', @@ -1187,6 +1225,7 @@ FW_VERSIONS = { b'\xf1\x870CW300042H \xf1\x891601', b'\xf1\x870D9300011T \xf1\x894801', b'\xf1\x870D9300012 \xf1\x894940', + b'\xf1\x870D9300013A \xf1\x894905', b'\xf1\x870D9300041H \xf1\x894905', b'\xf1\x870GC300043 \xf1\x892301', b'\xf1\x870D9300043F \xf1\x895202', @@ -1209,6 +1248,7 @@ FW_VERSIONS = { b'\xf1\x873Q0907572B \xf1\x890192', b'\xf1\x873Q0907572B \xf1\x890194', b'\xf1\x873Q0907572C \xf1\x890195', + b'\xf1\x875Q0907572R \xf1\x890771', ], }, } diff --git a/selfdrive/controls/lib/drive_helpers.py b/selfdrive/controls/lib/drive_helpers.py index 05c3897335..cd6525949c 100644 --- a/selfdrive/controls/lib/drive_helpers.py +++ b/selfdrive/controls/lib/drive_helpers.py @@ -190,3 +190,13 @@ def get_lag_adjusted_curvature(CP, v_ego, psis, curvatures, curvature_rates): current_curvature_desired + max_curvature_rate * DT_MDL) return safe_desired_curvature, safe_desired_curvature_rate + + +def get_friction(lateral_accel_error: float, lateral_accel_deadzone: float, friction_threshold: float, torque_params: car.CarParams.LateralTorqueTuning, friction_compensation: bool) -> float: + friction_interp = interp( + apply_center_deadzone(lateral_accel_error, lateral_accel_deadzone), + [-friction_threshold, friction_threshold], + [-torque_params.friction, torque_params.friction] + ) + friction = float(friction_interp) if friction_compensation else 0.0 + return friction diff --git a/selfdrive/controls/lib/events.py b/selfdrive/controls/lib/events.py index e4ddfb5326..89fc8d8357 100644 --- a/selfdrive/controls/lib/events.py +++ b/selfdrive/controls/lib/events.py @@ -658,6 +658,11 @@ EVENTS: Dict[int, Dict[str, Union[Alert, AlertCallbackType]]] = { ET.NO_ENTRY: NoEntryAlert("Steering Temporarily Unavailable"), }, + EventName.steerTimeLimit: { + ET.SOFT_DISABLE: soft_disable_alert("Vehicle Steering Time Limit"), + ET.NO_ENTRY: NoEntryAlert("Vehicle Steering Time Limit"), + }, + EventName.outOfSpace: { ET.PERMANENT: out_of_space_alert, ET.NO_ENTRY: NoEntryAlert("Out of Storage"), @@ -714,7 +719,7 @@ EVENTS: Dict[int, Dict[str, Union[Alert, AlertCallbackType]]] = { EventName.calibrationIncomplete: { ET.PERMANENT: calibration_incomplete_alert, - ET.SOFT_DISABLE: soft_disable_alert("Calibration in Progress"), + ET.SOFT_DISABLE: soft_disable_alert("Device remount detected: recalibrating"), ET.NO_ENTRY: NoEntryAlert("Calibration in Progress"), }, @@ -811,10 +816,6 @@ EVENTS: Dict[int, Dict[str, Union[Alert, AlertCallbackType]]] = { ET.NO_ENTRY: NoEntryAlert("Cruise Fault: Restart the Car"), }, - EventName.accFaultedTemp: { - ET.NO_ENTRY: NoEntryAlert("Cruise Temporarily Faulted"), - }, - EventName.controlsMismatch: { ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Controls Mismatch"), ET.NO_ENTRY: NoEntryAlert("Controls Mismatch"), diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 2f56094379..6550b19227 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -61,10 +61,12 @@ class LatControlTorque(LatControl): low_speed_factor = interp(CS.vEgo, LOW_SPEED_X, LOW_SPEED_Y)**2 setpoint = desired_lateral_accel + low_speed_factor * desired_curvature measurement = actual_lateral_accel + low_speed_factor * actual_curvature - error = setpoint - measurement gravity_adjusted_lateral_accel = desired_lateral_accel - params.roll * ACCELERATION_DUE_TO_GRAVITY - pid_log.error = self.torque_from_lateral_accel(error, self.torque_params, error, + torque_from_setpoint = self.torque_from_lateral_accel(setpoint, self.torque_params, setpoint, lateral_accel_deadzone, friction_compensation=False) + torque_from_measurement = self.torque_from_lateral_accel(measurement, self.torque_params, measurement, + lateral_accel_deadzone, friction_compensation=False) + pid_log.error = torque_from_setpoint - torque_from_measurement ff = self.torque_from_lateral_accel(gravity_adjusted_lateral_accel, self.torque_params, desired_lateral_accel - actual_lateral_accel, lateral_accel_deadzone, friction_compensation=True) diff --git a/selfdrive/controls/tests/test_startup.py b/selfdrive/controls/tests/test_startup.py index 92fc2468bb..18c8e79026 100755 --- a/selfdrive/controls/tests/test_startup.py +++ b/selfdrive/controls/tests/test_startup.py @@ -72,7 +72,6 @@ class TestStartup(unittest.TestCase): params.clear_all() params.put_bool("Passive", False) params.put_bool("OpenpilotEnabledToggle", True) - params.put_bool("ObdMultiplexingDisabled", True) # Build capnn version of FW array if fw_versions is not None: @@ -109,6 +108,10 @@ class TestStartup(unittest.TestCase): finger = _FINGERPRINTS[car_model][0] for _ in range(1000): + # controlsd waits for boardd to echo back that it has changed the multiplexing mode + if not params.get_bool("ObdMultiplexingChanged"): + params.put_bool("ObdMultiplexingChanged", True) + msgs = [[addr, 0, b'\x00'*length, 0] for addr, length in finger.items()] pm.send('can', can_list_to_can_capnp(msgs)) diff --git a/selfdrive/debug/profiling/watch-irqs.sh b/selfdrive/debug/profiling/watch-irqs.sh new file mode 100755 index 0000000000..34cc4596f4 --- /dev/null +++ b/selfdrive/debug/profiling/watch-irqs.sh @@ -0,0 +1,4 @@ +#!/usr/bin/bash +set -e + +RUBYOPT="-W0" irqtop -d1 -R diff --git a/selfdrive/debug/test_car_model.py b/selfdrive/debug/test_car_model.py index 4de5b26762..c2f51c9355 100755 --- a/selfdrive/debug/test_car_model.py +++ b/selfdrive/debug/test_car_model.py @@ -6,6 +6,7 @@ import unittest from selfdrive.car.tests.routes import CarTestRoute from selfdrive.car.tests.test_models import TestCarModel +from tools.lib.route import SegmentName def create_test_models_suite(routes: List[Tuple[str, CarTestRoute]], ci=False) -> unittest.TestSuite: @@ -21,16 +22,17 @@ def create_test_models_suite(routes: List[Tuple[str, CarTestRoute]], ci=False) - if __name__ == "__main__": parser = argparse.ArgumentParser(description="Test any route against common issues with a new car port. " + "Uses selfdrive/car/tests/test_models.py") - parser.add_argument("route", help="Specify route to run tests on") + parser.add_argument("route_or_segment_name", help="Specify route to run tests on") parser.add_argument("--car", help="Specify car model for test route") - parser.add_argument("--segment", type=int, nargs="?", help="Specify segment of route to test") parser.add_argument("--ci", action="store_true", help="Attempt to get logs using openpilotci, need to specify car") args = parser.parse_args() if len(sys.argv) == 1: parser.print_help() sys.exit() - test_route = CarTestRoute(args.route, args.car, segment=args.segment) + route_or_segment_name = SegmentName(args.route_or_segment_name.strip(), allow_route_name=True) + segment_num = route_or_segment_name.segment_num if route_or_segment_name.segment_num != -1 else None + test_route = CarTestRoute(route_or_segment_name.route_name.canonical_name, args.car, segment=segment_num) test_suite = create_test_models_suite([(args.car, test_route)], ci=args.ci) unittest.TextTestRunner().run(test_suite) diff --git a/selfdrive/debug/test_fw_query_on_routes.py b/selfdrive/debug/test_fw_query_on_routes.py index dc6688324f..0bb9aa5e54 100755 --- a/selfdrive/debug/test_fw_query_on_routes.py +++ b/selfdrive/debug/test_fw_query_on_routes.py @@ -8,13 +8,11 @@ import traceback from tqdm import tqdm from tools.lib.logreader import LogReader from tools.lib.route import Route -from selfdrive.car.interfaces import get_interface_attr from selfdrive.car.car_helpers import interface_names -from selfdrive.car.fw_versions import match_fw_to_car +from selfdrive.car.fw_versions import VERSIONS, match_fw_to_car NO_API = "NO_API" in os.environ -VERSIONS = get_interface_attr('FW_VERSIONS', ignore_none=True) SUPPORTED_BRANDS = VERSIONS.keys() SUPPORTED_CARS = [brand for brand in SUPPORTED_BRANDS for brand in interface_names[brand]] UNKNOWN_BRAND = "unknown" diff --git a/selfdrive/locationd/calibrationd.py b/selfdrive/locationd/calibrationd.py index 1c68eb67bd..091f211175 100755 --- a/selfdrive/locationd/calibrationd.py +++ b/selfdrive/locationd/calibrationd.py @@ -25,7 +25,7 @@ MAX_VEL_ANGLE_STD = np.radians(0.25) MAX_YAW_RATE_FILTER = np.radians(2) # per second # This is at model frequency, blocks needed for efficiency -SMOOTH_CYCLES = 400 +SMOOTH_CYCLES = 10 BLOCK_SIZE = 100 INPUTS_NEEDED = 5 # Minimum blocks needed for valid calibration INPUTS_WANTED = 50 # We want a little bit more than we need for stability @@ -143,7 +143,7 @@ class Calibrator: # If spread is too high, assume mounting was changed and reset to last block. # Make the transition smooth. Abrupt transitions are not good for feedback loop through supercombo model. if max(self.calib_spread) > MAX_ALLOWED_SPREAD and self.cal_status == Calibration.CALIBRATED: - self.reset(self.rpys[self.block_idx - 1], valid_blocks=INPUTS_NEEDED, smooth_from=self.rpy) + self.reset(self.rpys[self.block_idx - 1], valid_blocks=1, smooth_from=self.rpy) write_this_cycle = (self.idx == 0) and (self.block_idx % (INPUTS_WANTED//5) == 5) if self.param_put and write_this_cycle: @@ -162,7 +162,7 @@ class Calibrator: rot: List[float], wide_from_device_euler: List[float], trans_std: List[float]) -> Optional[np.ndarray]: - self.old_rpy_weight = min(0.0, self.old_rpy_weight - 1/SMOOTH_CYCLES) + self.old_rpy_weight = max(0.0, self.old_rpy_weight - 1/SMOOTH_CYCLES) straight_and_fast = ((self.v_ego > MIN_SPEED_FILTER) and (trans[0] > MIN_SPEED_FILTER) and (abs(rot[2]) < MAX_YAW_RATE_FILTER)) angle_std_threshold = MAX_VEL_ANGLE_STD diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 87b0cf9291..8b07c46aaa 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -7,7 +7,7 @@ from collections import defaultdict from concurrent.futures import Future, ProcessPoolExecutor from datetime import datetime from enum import IntEnum -from typing import List, Optional +from typing import List, Optional, Dict, Any import numpy as np @@ -116,7 +116,7 @@ class Laikad: nav_dict = {} try: ephem_cache = ephemeris_structs.EphemerisCache.from_bytes(cache_bytes) - glonass_navs = [GLONASSEphemeris(data_struct, file_name=EPHEMERIS_CACHE) for data_struct in ephem_cache.glonassEphemerides] + glonass_navs = [GLONASSEphemeris(data_struct, file_name=EPHEMERIS_CACHE) for data_struct in ephem_cache.glonassEphemerides] gps_navs = [GPSEphemeris(data_struct, file_name=EPHEMERIS_CACHE) for data_struct in ephem_cache.gpsEphemerides] for e in sum([glonass_navs, gps_navs], []): if e.prn not in nav_dict: @@ -154,7 +154,7 @@ class Laikad: ephemeris_statuses.append(status) return ephemeris_statuses - + def get_lsq_fix(self, t, measurements): if self.last_fix_t is None or abs(self.last_fix_t - t) > 0: min_measurements = 5 if any(p.constellation_id == ConstellationId.GLONASS for p in measurements) else 4 @@ -220,16 +220,28 @@ class Laikad: # TODO this is not robust to gps week rollover if self.gps_week is None: return - ephem = parse_qcom_ephem(gnss_msg.drSvPoly, self.gps_week) - self.astro_dog.add_qcom_polys({ephem.prn: [ephem]}) + try: + ephem = parse_qcom_ephem(gnss_msg.drSvPoly, self.gps_week) + self.astro_dog.add_qcom_polys({ephem.prn: [ephem]}) + except Exception: + cloudlog.exception("Error parsing qcom svPoly ephemeris from qcom module") + return else: if gnss_msg.which() == 'ephemeris': data_struct = ephemeris_structs.Ephemeris.new_message(**gnss_msg.ephemeris.to_dict()) - ephem = GPSEphemeris(data_struct, file_name='ublox') + try: + ephem = GPSEphemeris(data_struct, file_name='ublox') + except Exception: + cloudlog.exception("Error parsing GPS ephemeris from ublox") + return elif gnss_msg.which() == 'glonassEphemeris': data_struct = ephemeris_structs.GlonassEphemeris.new_message(**gnss_msg.glonassEphemeris.to_dict()) - ephem = GLONASSEphemeris(data_struct, file_name='ublox') + try: + ephem = GLONASSEphemeris(data_struct, file_name='ublox') + except Exception: + cloudlog.exception("Error parsing GLONASS ephemeris from ublox") + return else: cloudlog.error(f"Unsupported ephemeris type: {gnss_msg.which()}") return @@ -241,12 +253,16 @@ class Laikad: new_meas = [m for m in new_meas if 1e7 < m.observables['C1C'] < 3e7] processed_measurements = process_measurements(new_meas, self.astro_dog) if self.last_fix_pos is not None: - corrected_measurements = correct_measurements(processed_measurements, self.last_fix_pos, self.astro_dog) - instant_fix = self.get_lsq_fix(t, corrected_measurements) - #instant_fix = self.get_lsq_fix(t, processed_measurements) + est_pos = self.last_fix_pos + correct_delay = True else: - corrected_measurements = [] - instant_fix = self.get_lsq_fix(t, processed_measurements) + est_pos = self.gnss_kf.x[GStates.ECEF_POS].tolist() + correct_delay = False + corrected_measurements = correct_measurements(processed_measurements, est_pos, self.astro_dog, correct_delay=correct_delay) + return corrected_measurements + + def calc_fix(self, t, measurements): + instant_fix = self.get_lsq_fix(t, measurements) if instant_fix is None: return None else: @@ -254,64 +270,52 @@ class Laikad: self.last_fix_t = t self.last_fix_pos = position_estimate self.lat_fix_pos_std = position_std - if (t*1e9) % 10 == 0: - cloudlog.debug(f"Measurements Incoming/Processed/Corrected: {len(new_meas), len(processed_measurements), len(corrected_measurements)}") - return position_estimate, position_std, velocity_estimate, velocity_std, corrected_measurements, processed_measurements + return position_estimate, position_std, velocity_estimate, velocity_std def process_gnss_msg(self, gnss_msg, gnss_mono_time: int, block=False): out_msg = messaging.new_message("gnssMeasurements") - out_msg.gnssMeasurements = { - "timeToFirstFix": self.ttff, - "ephemerisStatuses" : self.create_ephem_statuses(), - } + t = gnss_mono_time * 1e-9 + msg_dict: Dict[str, Any] = {"measTime": gnss_mono_time} if self.first_log_time is None: self.first_log_time = 1e-9 * gnss_mono_time if self.is_ephemeris(gnss_msg): self.read_ephemeris(gnss_msg) - return out_msg elif self.is_good_report(gnss_msg): week, tow, new_meas = self.read_report(gnss_msg) self.gps_week = week - if len(new_meas) == 0: - return out_msg - - t = gnss_mono_time * 1e-9 if week > 0: self.got_first_gnss_msg = True latest_msg_t = GPSTime(week, tow) if self.auto_fetch_navs: self.fetch_navs(latest_msg_t, block) - output = self.process_report(new_meas, t) - if output is None: - return out_msg - if self.ttff <= 0: - self.ttff = max(1e-3, t - self.first_log_time) - position_estimate, position_std, velocity_estimate, velocity_std, corrected_measurements, _ = output + corrected_measurements = self.process_report(new_meas, t) + msg_dict['correctedMeasurements'] = [create_measurement_msg(m) for m in corrected_measurements] - self.update_localizer(position_estimate, t, corrected_measurements) - meas_msgs = [create_measurement_msg(m) for m in corrected_measurements] + fix = self.calc_fix(t, corrected_measurements) measurement_msg = log.LiveLocationKalman.Measurement.new_message - + if fix is not None: + position_estimate, position_std, velocity_estimate, velocity_std = fix + if self.ttff <= 0: + self.ttff = max(1e-3, t - self.first_log_time) + msg_dict["positionECEF"] = measurement_msg(value=position_estimate, std=position_std.tolist(), valid=bool(self.last_fix_t == t)) + msg_dict["velocityECEF"] = measurement_msg(value=velocity_estimate, std=velocity_std.tolist(), valid=bool(self.last_fix_t == t)) + + self.update_localizer(self.last_fix_pos, t, corrected_measurements) P_diag = self.gnss_kf.P.diagonal() kf_valid = all(self.kf_valid(t)) - out_msg.gnssMeasurements = { - "gpsWeek": week, - "gpsTimeOfWeek": tow, - "kalmanPositionECEF": measurement_msg(value=self.gnss_kf.x[GStates.ECEF_POS].tolist(), + msg_dict["kalmanPositionECEF"] = measurement_msg(value=self.gnss_kf.x[GStates.ECEF_POS].tolist(), std=np.sqrt(P_diag[GStates.ECEF_POS]).tolist(), - valid=kf_valid), - "kalmanVelocityECEF": measurement_msg(value=self.gnss_kf.x[GStates.ECEF_VELOCITY].tolist(), + valid=kf_valid) + msg_dict["kalmanVelocityECEF"] = measurement_msg(value=self.gnss_kf.x[GStates.ECEF_VELOCITY].tolist(), std=np.sqrt(P_diag[GStates.ECEF_VELOCITY]).tolist(), - valid=kf_valid), - "positionECEF": measurement_msg(value=position_estimate, std=position_std.tolist(), valid=bool(self.last_fix_t == t)), - "velocityECEF": measurement_msg(value=velocity_estimate, std=velocity_std.tolist(), valid=bool(self.last_fix_t == t)), - - "measTime": gnss_mono_time, - "correctedMeasurements": meas_msgs, - "timeToFirstFix": self.ttff, - "ephemerisStatuses" : self.create_ephem_statuses(), - } + valid=kf_valid) + + msg_dict['gpsWeek'] = self.last_report_time.week + msg_dict['gpsTimeOfWeek'] = self.last_report_time.tow + msg_dict['timeToFirstFix'] = self.ttff + msg_dict['ephemerisStatuses'] = self.create_ephem_statuses() + out_msg.gnssMeasurements = msg_dict return out_msg def update_localizer(self, est_pos, t: float, measurements: List[GNSSMeasurement]): @@ -324,7 +328,7 @@ class Laikad: cloudlog.error("Time gap of over 10s detected, gnss kalman reset") elif not valid[2]: cloudlog.error("Gnss kalman filter state is nan") - if len(est_pos) > 0: + if est_pos is not None and len(est_pos) > 0: cloudlog.info(f"Reset kalman filter with {est_pos}") self.init_gnss_localizer(est_pos) else: @@ -420,22 +424,23 @@ def clear_tmp_cache(): os.mkdir(DOWNLOADS_CACHE_FOLDER) -def main(sm=None, pm=None, qc=None): +def main(sm=None, pm=None): #clear_tmp_cache() use_qcom = not Params().get_bool("UbloxAvailable", block=True) - if use_qcom or (qc is not None and qc): - raw_gnss_socket = "qcomGnss" + if use_qcom: + raw_name = "qcomGnss" else: - raw_gnss_socket = "ubloxGnss" + raw_name = "ubloxGnss" + raw_gnss_sock = messaging.sub_sock(raw_name, conflate=False, timeout=1000) if sm is None: - sm = messaging.SubMaster([raw_gnss_socket, 'clocks']) + sm = messaging.SubMaster(['clocks',]) if pm is None: pm = messaging.PubMaster(['gnssMeasurements']) # disable until set as main gps source, to better analyze startup time - use_internet = False #"LAIKAD_NO_INTERNET" not in os.environ + use_internet = False # "LAIKAD_NO_INTERNET" not in os.environ replay = "REPLAY" in os.environ if replay or "CI" in os.environ: @@ -444,18 +449,17 @@ def main(sm=None, pm=None, qc=None): laikad = Laikad(save_ephemeris=not replay, auto_fetch_navs=use_internet, use_qcom=use_qcom) while True: - sm.update() - - if sm.updated[raw_gnss_socket]: - gnss_msg = sm[raw_gnss_socket] - msg = laikad.process_gnss_msg(gnss_msg, sm.logMonoTime[raw_gnss_socket], replay) - pm.send('gnssMeasurements', msg) + for in_msg in messaging.drain_sock(raw_gnss_sock): + out_msg = laikad.process_gnss_msg(getattr(in_msg, raw_name), in_msg.logMonoTime, replay) + pm.send('gnssMeasurements', out_msg) + sm.update(0) if not laikad.got_first_gnss_msg and sm.updated['clocks']: clocks_msg = sm['clocks'] t = GPSTime.from_datetime(datetime.utcfromtimestamp(clocks_msg.wallTimeNanos * 1E-9)) if laikad.auto_fetch_navs: laikad.fetch_navs(t, block=replay) + if __name__ == "__main__": main() diff --git a/selfdrive/locationd/locationd.cc b/selfdrive/locationd/locationd.cc index 307626506a..6c0307bd64 100755 --- a/selfdrive/locationd/locationd.cc +++ b/selfdrive/locationd/locationd.cc @@ -27,7 +27,6 @@ const double MAX_FILTER_REWIND_TIME = 0.8; // s // They should be replaced with synced time from a real clock const double GPS_QUECTEL_SENSOR_TIME_OFFSET = 0.630; // s const double GPS_UBLOX_SENSOR_TIME_OFFSET = 0.095; // s -const float GPS_MUL_FACTOR = 10.0; const float GPS_POS_STD_THRESHOLD = 50.0; const float GPS_VEL_STD_THRESHOLD = 5.0; const float GPS_POS_ERROR_RESET_THRESHOLD = 300.0; @@ -181,6 +180,7 @@ void Localizer::build_live_location(cereal::LiveLocationKalman::Builder& fix) { fix.setPosenetOK(!(std_spike && this->car_speed > 5.0)); fix.setDeviceStable(!this->device_fell); fix.setExcessiveResets(this->reset_tracker > MAX_RESET_TRACKER); + fix.setTimeToFirstFix(std::isnan(this->ttff) ? -1. : this->ttff); this->device_fell = false; //fix.setGpsWeek(this->time.week); @@ -304,13 +304,7 @@ void Localizer::handle_gps(double current_time, const cereal::GpsLocationData::R bool gps_lat_lng_alt_insane = ((std::abs(log.getLatitude()) > 90) || (std::abs(log.getLongitude()) > 180) || (std::abs(log.getAltitude()) > ALTITUDE_SANITY_CHECK)); bool gps_vel_insane = (floatlist2vector(log.getVNED()).norm() > TRANS_SANITY_CHECK); - // quectel gps verticalAccuracy is clipped to 500 - bool gps_accuracy_insane_quectel = false; - if (!ublox_available) { - gps_accuracy_insane_quectel = log.getVerticalAccuracy() == 500; - } - - if (gps_invalid_flag || gps_unreasonable || gps_accuracy_insane || gps_lat_lng_alt_insane || gps_vel_insane || gps_accuracy_insane_quectel) { + if (gps_invalid_flag || gps_unreasonable || gps_accuracy_insane || gps_lat_lng_alt_insane || gps_vel_insane) { //this->gps_valid = false; this->determine_gps_mode(current_time); return; @@ -326,8 +320,14 @@ void Localizer::handle_gps(double current_time, const cereal::GpsLocationData::R VectorXd ecef_pos = this->converter->ned2ecef({ 0.0, 0.0, 0.0 }).to_vector(); VectorXd ecef_vel = this->converter->ned2ecef({ log.getVNED()[0], log.getVNED()[1], log.getVNED()[2] }).to_vector() - ecef_pos; - MatrixXdr ecef_pos_R = Vector3d::Constant(std::pow(GPS_MUL_FACTOR * log.getAccuracy(),2) + std::pow(GPS_MUL_FACTOR * log.getVerticalAccuracy(),2)).asDiagonal(); - MatrixXdr ecef_vel_R = Vector3d::Constant(std::pow(GPS_MUL_FACTOR * log.getSpeedAccuracy(), 2)).asDiagonal(); + float ecef_pos_std; + if (ublox_available) { + ecef_pos_std = std::sqrt(std::pow(log.getAccuracy(), 2) + std::pow(log.getVerticalAccuracy(), 2)); + } else { + ecef_pos_std = std::sqrt(3 * std::pow(log.getVerticalAccuracy(), 2)); + } + MatrixXdr ecef_pos_R = Vector3d::Constant(std::pow(this->gps_std_factor * ecef_pos_std, 2)).asDiagonal(); + MatrixXdr ecef_vel_R = Vector3d::Constant(std::pow(this->gps_std_factor * log.getSpeedAccuracy(), 2)).asDiagonal(); this->unix_timestamp_millis = log.getUnixTimestampMillis(); double gps_est_error = (this->kf->get_x().segment(STATE_ECEF_POS_START) - ecef_pos).norm(); @@ -377,14 +377,14 @@ void Localizer::handle_gnss(double current_time, const cereal::GnssMeasurements: // indexed at 0 cause all std values are the same MAE auto ecef_pos_std = log.getPositionECEF().getStd()[0]; - MatrixXdr ecef_pos_R = Vector3d::Constant(pow(GPS_MUL_FACTOR*ecef_pos_std, 2)).asDiagonal(); + MatrixXdr ecef_pos_R = Vector3d::Constant(pow(this->gps_std_factor*ecef_pos_std, 2)).asDiagonal(); auto ecef_vel_v = log.getVelocityECEF().getValue(); VectorXd ecef_vel = Vector3d(ecef_vel_v[0], ecef_vel_v[1], ecef_vel_v[2]); // indexed at 0 cause all std values are the same MAE auto ecef_vel_std = log.getVelocityECEF().getStd()[0]; - MatrixXdr ecef_vel_R = Vector3d::Constant(pow(GPS_MUL_FACTOR*ecef_vel_std, 2)).asDiagonal(); + MatrixXdr ecef_vel_R = Vector3d::Constant(pow(this->gps_std_factor*ecef_vel_std, 2)).asDiagonal(); double gps_est_error = (this->kf->get_x().segment(STATE_ECEF_POS_START) - ecef_pos).norm(); @@ -530,6 +530,9 @@ void Localizer::time_check(double current_time) { if (std::isnan(this->last_reset_time)) { this->last_reset_time = current_time; } + if (std::isnan(this->first_valid_log_time)) { + this->first_valid_log_time = current_time; + } double filter_time = this->kf->get_filter_time(); bool big_time_gap = !std::isnan(filter_time) && (current_time - filter_time > 10); if (big_time_gap) { @@ -661,8 +664,10 @@ int Localizer::locationd_thread() { const char* gps_location_socket; if (ublox_available) { gps_location_socket = "gpsLocationExternal"; + this->gps_std_factor = 10.0; } else { gps_location_socket = "gpsLocation"; + this->gps_std_factor = 2.0; } const std::initializer_list service_list = {gps_location_socket, "cameraOdometry", "liveCalibration", "carState", "carParams", "accelerometer", "gyroscope"}; @@ -699,6 +704,11 @@ int Localizer::locationd_thread() { bool gpsOK = this->is_gps_ok(); bool sensorsOK = sm.allAliveAndValid({"accelerometer", "gyroscope"}); + // Log time to first fix + if (gpsOK && std::isnan(this->ttff) && !std::isnan(this->first_valid_log_time)) { + this->ttff = std::max(1e-3, (sm[trigger_msg].getLogMonoTime() * 1e-9) - this->first_valid_log_time); + } + MessageBuilder msg_builder; kj::ArrayPtr bytes = this->get_message_bytes(msg_builder, inputsOK, sensorsOK, gpsOK, filterInitialized); pm.send("liveLocationKalman", bytes.begin(), bytes.size()); diff --git a/selfdrive/locationd/locationd.h b/selfdrive/locationd/locationd.h index b7e42a9df8..e2b2096afc 100755 --- a/selfdrive/locationd/locationd.h +++ b/selfdrive/locationd/locationd.h @@ -78,10 +78,13 @@ private: double reset_tracker = 0.0; bool device_fell = false; bool gps_mode = false; + double first_valid_log_time = NAN; + double ttff = NAN; double last_gps_msg = 0; bool ublox_available = true; bool observation_timings_invalid = false; std::map observation_values_invalid; bool standstill = true; int32_t orientation_reset_count = 0; + float gps_std_factor; }; diff --git a/selfdrive/locationd/test/test_calibrationd.py b/selfdrive/locationd/test/test_calibrationd.py index 3612f48276..2c6508fd42 100755 --- a/selfdrive/locationd/test/test_calibrationd.py +++ b/selfdrive/locationd/test/test_calibrationd.py @@ -6,7 +6,7 @@ import numpy as np import cereal.messaging as messaging from common.params import Params -from selfdrive.locationd.calibrationd import Calibrator +from selfdrive.locationd.calibrationd import Calibrator, INPUTS_NEEDED, INPUTS_WANTED, BLOCK_SIZE, MIN_SPEED_FILTER, MAX_YAW_RATE_FILTER, SMOOTH_CYCLES class TestCalibrationd(unittest.TestCase): @@ -22,5 +22,81 @@ class TestCalibrationd(unittest.TestCase): self.assertEqual(msg.liveCalibration.validBlocks, c.valid_blocks) + def test_calibration_basics(self): + c = Calibrator(param_put=False) + for _ in range(BLOCK_SIZE * INPUTS_WANTED): + c.handle_v_ego(MIN_SPEED_FILTER + 1) + c. handle_cam_odom([MIN_SPEED_FILTER + 1, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [1e-3, 1e-3, 1e-3]) + self.assertEqual(c.valid_blocks, INPUTS_WANTED) + np.testing.assert_allclose(c.rpy, np.zeros(3)) + c.reset() + + def test_calibration_low_speed_reject(self): + c = Calibrator(param_put=False) + for _ in range(BLOCK_SIZE * INPUTS_WANTED): + c.handle_v_ego(MIN_SPEED_FILTER - 1) + c. handle_cam_odom([MIN_SPEED_FILTER + 1, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [1e-3, 1e-3, 1e-3]) + for _ in range(BLOCK_SIZE * INPUTS_WANTED): + c.handle_v_ego(MIN_SPEED_FILTER + 1) + c. handle_cam_odom([MIN_SPEED_FILTER - 1, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [1e-3, 1e-3, 1e-3]) + self.assertEqual(c.valid_blocks, 0) + np.testing.assert_allclose(c.rpy, np.zeros(3)) + + + def test_calibration_yaw_rate_reject(self): + c = Calibrator(param_put=False) + for _ in range(BLOCK_SIZE * INPUTS_WANTED): + c.handle_v_ego(MIN_SPEED_FILTER + 1) + c. handle_cam_odom([MIN_SPEED_FILTER + 1, 0.0, 0.0], + [0.0, 0.0, MAX_YAW_RATE_FILTER ], + [0.0, 0.0, 0.0], + [1e-3, 1e-3, 1e-3]) + self.assertEqual(c.valid_blocks, 0) + np.testing.assert_allclose(c.rpy, np.zeros(3)) + + + def test_calibration_speed_std_reject(self): + c = Calibrator(param_put=False) + for _ in range(BLOCK_SIZE * INPUTS_WANTED): + c.handle_v_ego(MIN_SPEED_FILTER + 1) + c. handle_cam_odom([MIN_SPEED_FILTER + 1, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [1e3, 1e3, 1e3]) + self.assertEqual(c.valid_blocks, INPUTS_NEEDED) + np.testing.assert_allclose(c.rpy, np.zeros(3)) + + + def test_calibration_auto_reset(self): + c = Calibrator(param_put=False) + for _ in range(BLOCK_SIZE * INPUTS_WANTED): + c.handle_v_ego(MIN_SPEED_FILTER + 1) + c. handle_cam_odom([MIN_SPEED_FILTER + 1, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [1e-3, 1e-3, 1e-3]) + self.assertEqual(c.valid_blocks, INPUTS_WANTED) + np.testing.assert_allclose(c.rpy, [0.0, 0.0, 0.0]) + old_rpy_weight_prev = 0.0 + for _ in range(BLOCK_SIZE + 10): + self.assertLess(old_rpy_weight_prev - c.old_rpy_weight, 1/SMOOTH_CYCLES + 1e-3) + old_rpy_weight_prev = c.old_rpy_weight + c.handle_v_ego(MIN_SPEED_FILTER + 1) + c.handle_cam_odom([MIN_SPEED_FILTER + 1, -0.05 * MIN_SPEED_FILTER, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [1e-3, 1e-3, 1e-3]) + self.assertEqual(c.valid_blocks, 1) + np.testing.assert_allclose(c.rpy, [0.0, 0.0, -0.05], atol=1e-2) + if __name__ == "__main__": unittest.main() diff --git a/selfdrive/locationd/test/test_laikad.py b/selfdrive/locationd/test/test_laikad.py index 265db50c6d..e184dc3ec4 100755 --- a/selfdrive/locationd/test/test_laikad.py +++ b/selfdrive/locationd/test/test_laikad.py @@ -180,7 +180,7 @@ class TestLaikad(unittest.TestCase): laikad = Laikad(auto_update=True, valid_ephem_types=EphemerisType.ULTRA_RAPID_ORBIT) correct_msgs = verify_messages(self.logs, laikad) - correct_msgs_expected = 559 + correct_msgs_expected = 560 self.assertEqual(correct_msgs_expected, len(correct_msgs)) self.assertEqual(correct_msgs_expected, len([m for m in correct_msgs if m.gnssMeasurements.positionECEF.valid])) @@ -201,9 +201,11 @@ class TestLaikad(unittest.TestCase): laikad = Laikad(auto_update=True, valid_ephem_types=EphemerisType.NAV, use_qcom=use_qcom) # Disable fetch_orbits to test NAV only correct_msgs = verify_messages(logs, laikad) - correct_msgs_expected = 42 if use_qcom else 559 + correct_msgs_expected = 44 if use_qcom else 560 + valid_fix_expected = 43 if use_qcom else 560 + self.assertEqual(correct_msgs_expected, len(correct_msgs)) - self.assertEqual(correct_msgs_expected, len([m for m in correct_msgs if m.gnssMeasurements.positionECEF.valid])) + self.assertEqual(valid_fix_expected, len([m for m in correct_msgs if m.gnssMeasurements.positionECEF.valid])) @mock.patch('laika.downloader.download_and_cache_file') def test_laika_offline(self, downloader_mock): @@ -216,8 +218,9 @@ class TestLaikad(unittest.TestCase): downloader_mock.side_effect = DownloadFailed laikad = Laikad(auto_update=False) correct_msgs = verify_messages(self.logs, laikad) - self.assertEqual(375, len(correct_msgs)) - self.assertEqual(375, len([m for m in correct_msgs if m.gnssMeasurements.positionECEF.valid])) + expected_msgs = 376 + self.assertEqual(expected_msgs, len(correct_msgs)) + self.assertEqual(expected_msgs, len([m for m in correct_msgs if m.gnssMeasurements.positionECEF.valid])) def test_laika_get_orbits(self): laikad = Laikad(auto_update=False) @@ -325,7 +328,7 @@ class TestLaikad(unittest.TestCase): gm = msg.gnssMeasurements if len(gm.correctedMeasurements) != 0 and gm.positionECEF.valid: cnt += 1 - self.assertEqual(cnt, 559) + self.assertEqual(cnt, 560) def dict_has_values(self, dct): self.assertGreater(len(dct), 0) diff --git a/selfdrive/manager/manager.py b/selfdrive/manager/manager.py index 1b10cee806..e63053fefd 100755 --- a/selfdrive/manager/manager.py +++ b/selfdrive/manager/manager.py @@ -20,7 +20,8 @@ from selfdrive.manager.process_config import managed_processes from selfdrive.athena.registration import register, UNREGISTERED_DONGLE_ID from system.swaglog import cloudlog, add_file_handler from system.version import is_dirty, get_commit, get_version, get_origin, get_short_branch, \ - terms_version, training_version, is_tested_branch, is_release_branch + get_normalized_origin, terms_version, training_version, \ + is_tested_branch, is_release_branch @@ -92,7 +93,12 @@ def manager_init() -> None: # init logging sentry.init(sentry.SentryProject.SELFDRIVE) - cloudlog.bind_global(dongle_id=dongle_id, version=get_version(), dirty=is_dirty(), + cloudlog.bind_global(dongle_id=dongle_id, + version=get_version(), + origin=get_normalized_origin(), + branch=get_short_branch(), + commit=get_commit(), + dirty=is_dirty(), device=HARDWARE.get_device_type()) diff --git a/selfdrive/modeld/models/supercombo.onnx b/selfdrive/modeld/models/supercombo.onnx index a483fa4db4..13759c62f8 100644 --- a/selfdrive/modeld/models/supercombo.onnx +++ b/selfdrive/modeld/models/supercombo.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:736ddc08497d7596bae4d9515a8efb996676be80e67a6d34d632bb8af2ed3fa9 -size 45962515 +oid sha256:5121deb0d5c683b0fbee4c1cad7bc625953bf127b1383fb7599a6b644efd0aea +size 46011200 diff --git a/selfdrive/navd/SConscript b/selfdrive/navd/SConscript index 8a2c2a8a91..23b36adc0a 100644 --- a/selfdrive/navd/SConscript +++ b/selfdrive/navd/SConscript @@ -1,21 +1,20 @@ Import('qt_env', 'arch', 'common', 'messaging', 'visionipc', 'cereal', 'transformations') -base_libs = [common, messaging, cereal, visionipc, transformations, 'zmq', - 'capnp', 'kj', 'm', 'OpenCL', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"] - +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': - base_libs.append('EGL') + libs.append('EGL') if arch in ['larch64', 'x86_64']: if arch == 'x86_64': - rpath = [Dir(f"#third_party/mapbox-gl-native-qt/{arch}").srcnode().abspath] - qt_env["RPATH"] += rpath + rpath = Dir(f"#third_party/mapbox-gl-native-qt/{arch}").srcnode().abspath + map_env["RPATH"] += [rpath, ] style_path = File("style.json").abspath - qt_env['CXXFLAGS'].append(f'-DSTYLE_PATH=\\"{style_path}\\"') - qt_libs = ["qt_widgets", "qt_util", "qmapboxgl"] + base_libs - - nav_src = ["main.cc", "map_renderer.cc"] - qt_env.Program("map_renderer", nav_src, LIBS=qt_libs + ['common', 'json11']) + map_env['CXXFLAGS'].append(f'-DSTYLE_PATH=\\"{style_path}\\"') - qt_env.SharedLibrary("map_renderer", ["map_renderer.cc"], LIBS=qt_libs + ['common', 'messaging']) + map_env["RPATH"].append(Dir('.').abspath) + map_env["LIBPATH"].append(Dir('.').abspath) + maplib = map_env.SharedLibrary("maprender", ["map_renderer.cc"], LIBS=libs) + map_env.Program("map_renderer", ["main.cc", ], LIBS=[maplib[0].get_path(), ] + libs) diff --git a/selfdrive/test/process_replay/model_replay_ref_commit b/selfdrive/test/process_replay/model_replay_ref_commit index ad09668a46..bd026f5710 100644 --- a/selfdrive/test/process_replay/model_replay_ref_commit +++ b/selfdrive/test/process_replay/model_replay_ref_commit @@ -1 +1 @@ -ab64afd1abd1059c14f50c67b51e5ef89029f216 +82db08d52b155336e9a1dadd11485d5acdf2eba0 diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index 971ddb3fe2..3072ecd0c9 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -5,7 +5,8 @@ import sys import threading import time import signal -from collections import namedtuple +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Callable import capnp @@ -28,7 +29,20 @@ TIMEOUT = 15 PROC_REPLAY_DIR = os.path.dirname(os.path.abspath(__file__)) FAKEDATA = os.path.join(PROC_REPLAY_DIR, "fakedata/") -ProcessConfig = namedtuple('ProcessConfig', ['proc_name', 'pub_sub', 'ignore', 'init_callback', 'should_recv_callback', 'tolerance', 'fake_pubsubmaster', 'submaster_config', 'environ', 'subtest_name', "field_tolerances"], defaults=({}, {}, "", {})) +@dataclass +class ProcessConfig: + proc_name: str + pub_sub: Dict[str, List[str]] + ignore: List[str] + init_callback: Optional[Callable] + should_recv_callback: Optional[Callable] + tolerance: Optional[float] + fake_pubsubmaster: bool + submaster_config: Dict[str, List[str]] = field(default_factory=dict) + environ: Dict[str, str] = field(default_factory=dict) + subtest_name: str = "" + field_tolerances: Dict[str, float] = field(default_factory=dict) + timeout: int = 30 def wait_for_event(evt): @@ -238,21 +252,12 @@ def torqued_rcv_callback(msg, CP, cfg, fsm): return recv_socks, fsm.frame == 0 or msg.which() == 'liveLocationKalman' -def ublox_rcv_callback(msg): +def ublox_rcv_callback(msg, CP, cfg, fsm): msg_class, msg_id = msg.ubloxRaw[2:4] if (msg_class, msg_id) in {(1, 7 * 16)}: - return ["gpsLocationExternal"] + return ["gpsLocationExternal"], True elif (msg_class, msg_id) in {(2, 1 * 16 + 5), (10, 9)}: - return ["ubloxGnss"] - else: - return [] - - -def laika_rcv_callback(msg, CP, cfg, fsm): - if msg.which() == 'ubloxGnss' and msg.ubloxGnss.which() == "measurementReport": - return ["gnssMeasurements"], True - elif msg.which() == 'qcomGnss' and msg.qcomGnss.which() == "drMeasurementReport": - return ["gnssMeasurements"], True + return ["ubloxGnss"], True else: return [], False @@ -274,7 +279,7 @@ CONFIGS = [ fake_pubsubmaster=True, submaster_config={ 'ignore_avg_freq': ['radarState', 'longitudinalPlan', 'driverCameraState', 'driverMonitoringState'], # dcam is expected at 20 Hz - 'ignore_alive': ['wideRoadCameraState'], # TODO: Add to regen + 'ignore_alive': [], } ), ProcessConfig( @@ -331,7 +336,7 @@ CONFIGS = [ pub_sub={ "cameraOdometry": ["liveLocationKalman"], "accelerometer": [], "gyroscope": [], - "gpsLocationExternal": [], "liveCalibration": [], "carState": [], + "gpsLocationExternal": [], "liveCalibration": [], "carState": [], "gpsLocation": [], }, ignore=["logMonoTime", "valid"], init_callback=get_car_params, @@ -371,9 +376,10 @@ CONFIGS = [ }, ignore=["logMonoTime"], init_callback=get_car_params, - should_recv_callback=laika_rcv_callback, + should_recv_callback=None, tolerance=NUMPY_TOLERANCE, - fake_pubsubmaster=True, + fake_pubsubmaster=False, + timeout=60*10, # first messages are blocked on internet assistance ), ProcessConfig( proc_name="torqued", @@ -395,10 +401,10 @@ def replay_process(cfg, lr, fingerprint=None): if cfg.fake_pubsubmaster: return python_replay_process(cfg, lr, fingerprint) else: - return cpp_replay_process(cfg, lr, fingerprint) + return replay_process_with_sockets(cfg, lr, fingerprint) -def setup_env(simulation=False, CP=None, cfg=None, controlsState=None): +def setup_env(simulation=False, CP=None, cfg=None, controlsState=None, lr=None): params = Params() params.clear_all() params.put_bool("OpenpilotEnabledToggle", True) @@ -406,13 +412,19 @@ def setup_env(simulation=False, CP=None, cfg=None, controlsState=None): params.put_bool("DisengageOnAccelerator", True) params.put_bool("WideCameraOnly", False) params.put_bool("DisableLogging", False) - params.put_bool("UbloxAvailable", True) - params.put_bool("ObdMultiplexingDisabled", True) os.environ["NO_RADAR_SLEEP"] = "1" os.environ["REPLAY"] = "1" - os.environ['SKIP_FW_QUERY'] = "" - os.environ['FINGERPRINT'] = "" + os.environ["SKIP_FW_QUERY"] = "" + os.environ["FINGERPRINT"] = "" + + if lr is not None: + services = {m.which() for m in lr} + params.put_bool("UbloxAvailable", "ubloxGnss" in services) + + if lr is not None: + services = {m.which() for m in lr} + params.put_bool("UbloxAvailable", "ubloxGnss" in services) if cfg is not None: # Clear all custom processConfig environment variables @@ -464,12 +476,6 @@ def python_replay_process(cfg, lr, fingerprint=None): all_msgs = sorted(lr, key=lambda msg: msg.logMonoTime) pub_msgs = [msg for msg in all_msgs if msg.which() in list(cfg.pub_sub.keys())] - # laikad needs decision between submaster ubloxGnss and qcomGnss, prio given to ubloxGnss - if cfg.proc_name == "laikad": - args = (*args, not any(m.which() == "ubloxGnss" for m in pub_msgs)) - service = "qcomGnss" if args[2] else "ubloxGnss" - pub_msgs = [m for m in pub_msgs if m.which() == service or m.which() == 'clocks'] - controlsState = None initialized = False for msg in lr: @@ -485,10 +491,10 @@ def python_replay_process(cfg, lr, fingerprint=None): if fingerprint is not None: os.environ['SKIP_FW_QUERY'] = "1" os.environ['FINGERPRINT'] = fingerprint - setup_env(cfg=cfg, controlsState=controlsState) + setup_env(cfg=cfg, controlsState=controlsState, lr=lr) else: CP = [m for m in lr if m.which() == 'carParams'][0].carParams - setup_env(CP=CP, cfg=cfg, controlsState=controlsState) + setup_env(CP=CP, cfg=cfg, controlsState=controlsState, lr=lr) assert(type(managed_processes[cfg.proc_name]) is PythonProcess) managed_processes[cfg.proc_name].prepare() @@ -517,7 +523,7 @@ def python_replay_process(cfg, lr, fingerprint=None): recv_socks, should_recv = cfg.should_recv_callback(msg, CP, cfg, fsm) else: recv_socks = [s for s in cfg.pub_sub[msg.which()] if - (fsm.frame + 1) % int(service_list[msg.which()].frequency / service_list[s].frequency) == 0] + (fsm.frame + 1) % max(1, int(service_list[msg.which()].frequency / service_list[s].frequency)) == 0] should_recv = bool(len(recv_socks)) if msg.which() == 'can': @@ -540,49 +546,58 @@ def python_replay_process(cfg, lr, fingerprint=None): return log_msgs -def cpp_replay_process(cfg, lr, fingerprint=None): - sub_sockets = [s for _, sub in cfg.pub_sub.items() for s in sub] # We get responses here +def replay_process_with_sockets(cfg, lr, fingerprint=None): pm = messaging.PubMaster(cfg.pub_sub.keys()) + sub_sockets = [s for _, sub in cfg.pub_sub.items() for s in sub] + sockets = {s: messaging.sub_sock(s, timeout=100) for s in sub_sockets} all_msgs = sorted(lr, key=lambda msg: msg.logMonoTime) pub_msgs = [msg for msg in all_msgs if msg.which() in list(cfg.pub_sub.keys())] - log_msgs = [] # We need to fake SubMaster alive since we can't inject a fake clock - setup_env(simulation=True, cfg=cfg) + setup_env(simulation=True, cfg=cfg, lr=lr) + + if cfg.proc_name == "laikad": + ublox = Params().get_bool("UbloxAvailable") + keys = set(cfg.pub_sub.keys()) - ({"qcomGnss", } if ublox else {"ubloxGnss", }) + pub_msgs = [msg for msg in pub_msgs if msg.which() in keys] managed_processes[cfg.proc_name].prepare() managed_processes[cfg.proc_name].start() + log_msgs = [] try: - with Timeout(TIMEOUT, error_msg=f"timed out testing process {repr(cfg.proc_name)}"): - while not all(pm.all_readers_updated(s) for s in cfg.pub_sub.keys()): + # Wait for process to startup + with Timeout(10, error_msg=f"timed out waiting for process to start: {repr(cfg.proc_name)}"): + while not any(pm.all_readers_updated(s) for s in cfg.pub_sub.keys()): time.sleep(0) - # Make sure all subscribers are connected - sockets = {s: messaging.sub_sock(s, timeout=2000) for s in sub_sockets} - for s in sub_sockets: - messaging.recv_one_or_none(sockets[s]) + for s in sockets.values(): + messaging.recv_one_or_none(s) - for i, msg in enumerate(pub_msgs): - pm.send(msg.which(), msg.as_builder()) - - resp_sockets = cfg.pub_sub[msg.which()] if cfg.should_recv_callback is None else cfg.should_recv_callback(msg) - for s in resp_sockets: - response = messaging.recv_one_retry(sockets[s]) + # Do the replay + cnt = 0 + for msg in pub_msgs: + with Timeout(cfg.timeout, error_msg=f"timed out testing process {repr(cfg.proc_name)}, {cnt}/{len(pub_msgs)} msgs done"): + resp_sockets = cfg.pub_sub[msg.which()] + if cfg.should_recv_callback is not None: + resp_sockets, _ = cfg.should_recv_callback(msg, None, None, None) - if response is None: - print(f"Warning, no response received {i}") - else: + # Make sure all subscribers are connected + if len(log_msgs) == 0 and len(resp_sockets) > 0: + for s in sockets.values(): + messaging.recv_one_or_none(s) - response = response.as_builder() - response.logMonoTime = msg.logMonoTime - response = response.as_reader() - log_msgs.append(response) + pm.send(msg.which(), msg.as_builder()) + while not pm.all_readers_updated(msg.which()): + time.sleep(0) - if not len(resp_sockets): # We only need to wait if we didn't already wait for a response - while not pm.all_readers_updated(msg.which()): - time.sleep(0) + for s in resp_sockets: + m = messaging.recv_one_retry(sockets[s]) + m = m.as_builder() + m.logMonoTime = msg.logMonoTime + log_msgs.append(m.as_reader()) + cnt += 1 finally: managed_processes[cfg.proc_name].signal(signal.SIGKILL) managed_processes[cfg.proc_name].stop() diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index cbe2a016ce..2e1c439698 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -5775220ec2e62dcdedb92d96270f1380bbd88c39 +627aa0f54e377d1f3954c58e37c0a15b555e20b3 diff --git a/selfdrive/test/process_replay/regen.py b/selfdrive/test/process_replay/regen.py index c9f9c6c362..8de4eb43fc 100755 --- a/selfdrive/test/process_replay/regen.py +++ b/selfdrive/test/process_replay/regen.py @@ -14,7 +14,7 @@ from cereal.services import service_list from cereal.visionipc import VisionIpcServer, VisionStreamType from common.params import Params from common.realtime import Ratekeeper, DT_MDL, DT_DMON, sec_since_boot -from common.transformations.camera import eon_f_frame_size, eon_d_frame_size, tici_f_frame_size, tici_d_frame_size +from common.transformations.camera import eon_f_frame_size, eon_d_frame_size, tici_f_frame_size, tici_d_frame_size, tici_e_frame_size from panda.python import Panda from selfdrive.car.toyota.values import EPS_SCALE from selfdrive.manager.process import ensure_running @@ -123,8 +123,9 @@ def replay_cameras(lr, frs, disable_tqdm=False): ("driverCameraState", DT_DMON, eon_d_frame_size, VisionStreamType.VISION_STREAM_DRIVER, False), ] tici_cameras = [ - ("roadCameraState", DT_MDL, tici_f_frame_size, VisionStreamType.VISION_STREAM_ROAD, True), - ("driverCameraState", DT_MDL, tici_d_frame_size, VisionStreamType.VISION_STREAM_DRIVER, False), + ("roadCameraState", DT_MDL, tici_f_frame_size, VisionStreamType.VISION_STREAM_ROAD, False), + ("wideRoadCameraState", DT_MDL, tici_e_frame_size, VisionStreamType.VISION_STREAM_WIDE_ROAD, False), + ("driverCameraState", DT_DMON, tici_d_frame_size, VisionStreamType.VISION_STREAM_DRIVER, False), ] def replay_camera(s, stream, dt, vipc_server, frames, size, use_extra_client): @@ -311,15 +312,28 @@ def regen_segment(lr, frs=None, outdir=FAKEDATA, disable_tqdm=False): return seg_path -def regen_and_save(route, sidx, upload=False, use_route_meta=True, outdir=FAKEDATA, disable_tqdm=False): +def regen_and_save(route, sidx, upload=False, use_route_meta=False, outdir=FAKEDATA, disable_tqdm=False): if use_route_meta: r = Route(route) lr = LogReader(r.log_paths()[sidx]) fr = FrameReader(r.camera_paths()[sidx]) + if r.ecamera_paths()[sidx] is not None: + wfr = FrameReader(r.ecamera_paths()[sidx]) + else: + wfr = None else: lr = LogReader(f"cd:/{route.replace('|', '/')}/{sidx}/rlog.bz2") fr = FrameReader(f"cd:/{route.replace('|', '/')}/{sidx}/fcamera.hevc") - rpath = regen_segment(lr, {'roadCameraState': fr}, outdir=outdir, disable_tqdm=disable_tqdm) + device_type = next(iter(lr)).initData.deviceType + if device_type == 'tici': + wfr = FrameReader(f"cd:/{route.replace('|', '/')}/{sidx}/ecamera.hevc") + else: + wfr = None + + frs = {'roadCameraState': fr} + if wfr is not None: + frs['wideRoadCameraState'] = wfr + rpath = regen_segment(lr, frs, outdir=outdir, disable_tqdm=disable_tqdm) # compress raw rlog before uploading with open(os.path.join(rpath, "rlog"), "rb") as f: @@ -344,7 +358,8 @@ def regen_and_save(route, sidx, upload=False, use_route_meta=True, outdir=FAKEDA 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("route", type=str, help="The source route") parser.add_argument("seg", type=int, help="Segment in source route") args = parser.parse_args() - regen_and_save(args.route, args.seg, args.upload) + regen_and_save(args.route, args.seg, args.upload, outdir=args.outdir) diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 07117c2e7f..a2dd938e2f 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -18,7 +18,7 @@ from tools.lib.logreader import LogReader source_segments = [ ("BODY", "937ccb7243511b65|2022-05-24--16-03-09--1"), # COMMA.BODY ("HYUNDAI", "02c45f73a2e5c6e9|2021-01-01--19-08-22--1"), # HYUNDAI.SONATA - ("HYUNDAI2", "d545129f3ca90f28|2022-11-07--20-43-08--3"), # HYUNDAI.KIA_EV6 + ("HYUNDAI2", "d545129f3ca90f28|2022-11-07--20-43-08--3"), # HYUNDAI.KIA_EV6 (+ QCOM GPS) ("TOYOTA", "0982d79ebb0de295|2021-01-04--17-13-21--13"), # TOYOTA.PRIUS (INDI) ("TOYOTA2", "0982d79ebb0de295|2021-01-03--20-03-36--6"), # TOYOTA.RAV4 (LQR) ("TOYOTA3", "f7d7e3538cda1a2a|2021-08-16--08-55-34--6"), # TOYOTA.COROLLA_TSS2 @@ -70,7 +70,7 @@ def run_test_process(data): res = None if not args.upload_only: lr = LogReader.from_bytes(lr_dat) - res, log_msgs = test_process(cfg, lr, ref_log_path, cur_log_fn, args.ignore_fields, args.ignore_msgs) + res, log_msgs = test_process(cfg, lr, segment, ref_log_path, cur_log_fn, args.ignore_fields, args.ignore_msgs) # save logs so we can upload when updating refs save_log(cur_log_fn, log_msgs) @@ -88,7 +88,7 @@ def get_log_data(segment): return (segment, f.read()) -def test_process(cfg, lr, ref_log_path, new_log_path, ignore_fields=None, ignore_msgs=None): +def test_process(cfg, lr, segment, ref_log_path, new_log_path, ignore_fields=None, ignore_msgs=None): if ignore_fields is None: ignore_fields = [] if ignore_msgs is None: @@ -96,7 +96,10 @@ def test_process(cfg, lr, ref_log_path, new_log_path, ignore_fields=None, ignore ref_log_msgs = list(LogReader(ref_log_path)) - log_msgs = replay_process(cfg, lr) + try: + log_msgs = replay_process(cfg, lr) + except Exception as e: + raise Exception("failed on segment: " + segment) from e # check to make sure openpilot is engaged in the route if cfg.proc_name == "controlsd": diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index 07a3c61bc5..81d68aa877 100755 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -25,7 +25,7 @@ PROCS = { "./loggerd": 10.0, "./encoderd": 17.0, "./camerad": 14.5, - "./locationd": 9.1, + "./locationd": 11.0, "selfdrive.controls.plannerd": 16.5, "./_ui": 19.2, "selfdrive.locationd.paramsd": 9.0, diff --git a/selfdrive/thermald/power_monitoring.py b/selfdrive/thermald/power_monitoring.py index 85e9510eb7..06e2b5e8f9 100644 --- a/selfdrive/thermald/power_monitoring.py +++ b/selfdrive/thermald/power_monitoring.py @@ -17,6 +17,7 @@ VBATT_PAUSE_CHARGING = 11.8 # Lower limit on the LPF car battery volta VBATT_INSTANT_PAUSE_CHARGING = 7.0 # Lower limit on the instant car battery voltage measurements to avoid triggering on instant power loss MAX_TIME_OFFROAD_S = 30*3600 MIN_ON_TIME_S = 3600 +DELAY_SHUTDOWN_TIME_S = 300 # Wait at least DELAY_SHUTDOWN_TIME_S seconds after offroad_time to shutdown. VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S = 60 class PowerMonitoring: @@ -124,6 +125,7 @@ class PowerMonitoring: should_shutdown &= not ignition should_shutdown &= (not self.params.get_bool("DisablePowerDown")) should_shutdown &= in_car + should_shutdown &= offroad_time > DELAY_SHUTDOWN_TIME_S should_shutdown |= self.params.get_bool("ForcePowerDown") should_shutdown &= started_seen or (now > MIN_ON_TIME_S) return should_shutdown diff --git a/selfdrive/thermald/tests/test_power_monitoring.py b/selfdrive/thermald/tests/test_power_monitoring.py index d41e92454b..6b1be2d7ef 100755 --- a/selfdrive/thermald/tests/test_power_monitoring.py +++ b/selfdrive/thermald/tests/test_power_monitoring.py @@ -15,7 +15,7 @@ def mock_sec_since_boot(): with patch("common.realtime.sec_since_boot", new=mock_sec_since_boot): with patch("common.params.put_nonblocking", new=params.put): from selfdrive.thermald.power_monitoring import PowerMonitoring, CAR_BATTERY_CAPACITY_uWh, \ - CAR_CHARGING_RATE_W, VBATT_PAUSE_CHARGING + CAR_CHARGING_RATE_W, VBATT_PAUSE_CHARGING, DELAY_SHUTDOWN_TIME_S TEST_DURATION_S = 50 GOOD_VOLTAGE = 12 * 1e3 @@ -116,10 +116,9 @@ class TestPowerMonitoring(unittest.TestCase): self.assertFalse(pm.should_shutdown(ignition, True, start_time, False)) self.assertTrue(pm.should_shutdown(ignition, True, start_time, False)) - # Test to check policy of stopping charging when the car voltage is too low def test_car_voltage(self): POWER_DRAW = 0 # To stop shutting down for other reasons - TEST_TIME = 100 + TEST_TIME = 350 VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S = 50 with pm_patch("VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S", VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S, constant=True), pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW): pm = PowerMonitoring() @@ -130,8 +129,9 @@ class TestPowerMonitoring(unittest.TestCase): pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition) if i % 10 == 0: self.assertEqual(pm.should_shutdown(ignition, True, start_time, True), - (pm.car_voltage_mV < VBATT_PAUSE_CHARGING*1e3 and - (ssb - start_time) > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S)) + (pm.car_voltage_mV < VBATT_PAUSE_CHARGING * 1e3 and + (ssb - start_time) > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S and + (ssb - start_time) > DELAY_SHUTDOWN_TIME_S)) self.assertTrue(pm.should_shutdown(ignition, True, start_time, True)) # Test to check policy of not stopping charging when DisablePowerDown is set @@ -177,6 +177,26 @@ class TestPowerMonitoring(unittest.TestCase): if i % 10 == 0: self.assertFalse(pm.should_shutdown(ignition, False, ssb, False)) self.assertFalse(pm.should_shutdown(ignition, False, ssb, False)) + + def test_delay_shutdown_time(self): + pm = PowerMonitoring() + pm.car_battery_capacity_uWh = 0 + ignition = False + in_car = True + offroad_timestamp = ssb + started_seen = True + pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition) + + while ssb < offroad_timestamp + DELAY_SHUTDOWN_TIME_S: + self.assertFalse(pm.should_shutdown(ignition, in_car, + offroad_timestamp, + started_seen), + f"Should not shutdown before {DELAY_SHUTDOWN_TIME_S} seconds offroad time") + self.assertTrue(pm.should_shutdown(ignition, in_car, + offroad_timestamp, + started_seen), + f"Should shutdown after {DELAY_SHUTDOWN_TIME_S} seconds offroad time") + if __name__ == "__main__": unittest.main() diff --git a/selfdrive/ui/qt/offroad/onboarding.cc b/selfdrive/ui/qt/offroad/onboarding.cc index f3e50b572b..622dcdd352 100644 --- a/selfdrive/ui/qt/offroad/onboarding.cc +++ b/selfdrive/ui/qt/offroad/onboarding.cc @@ -40,7 +40,6 @@ void TrainingGuide::mouseReleaseEvent(QMouseEvent *e) { } void TrainingGuide::showEvent(QShowEvent *event) { - img_path = width() == WIDE_WIDTH ? "../assets/training_wide/" : "../assets/training/"; boundingRect = width() == WIDE_WIDTH ? boundingRectWide : boundingRectStandard; currentIndex = 0; diff --git a/selfdrive/ui/qt/offroad/onboarding.h b/selfdrive/ui/qt/offroad/onboarding.h index 48f4094899..1be1d8f397 100644 --- a/selfdrive/ui/qt/offroad/onboarding.h +++ b/selfdrive/ui/qt/offroad/onboarding.h @@ -73,7 +73,7 @@ private: QRect(108, 804, 426, 164), }; - QString img_path; + const QString img_path = "../assets/training/"; QVector boundingRect; QElapsedTimer click_timer; diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 63b87149d4..fd14ed15e2 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -43,10 +43,10 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { }, { "ExperimentalLongitudinalEnabled", - tr("Experimental openpilot Longitudinal Control"), - QString("%1
%2") - .arg(tr("WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB).")) - .arg(tr("On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when using experimental openpilot longitudinal control.")), + tr("openpilot Longitudinal Control (Alpha)"), + QString("%1

%2") + .arg(tr("WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).")) + .arg(tr("On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha.")), "../assets/offroad/icon_speed_limit.png", }, { @@ -193,6 +193,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { connect(resetCalibBtn, &ButtonControl::clicked, [&]() { if (ConfirmationDialog::confirm(tr("Are you sure you want to reset calibration?"), tr("Reset"), this)) { params.remove("CalibrationParams"); + params.remove("LiveTorqueParameters"); } }); addItem(resetCalibBtn); diff --git a/selfdrive/ui/qt/util.cc b/selfdrive/ui/qt/util.cc index 4f52310649..2e222f5956 100644 --- a/selfdrive/ui/qt/util.cc +++ b/selfdrive/ui/qt/util.cc @@ -110,7 +110,7 @@ void sigTermHandler(int s) { qApp->quit(); } -void initApp(int argc, char *argv[]) { +void initApp(int argc, char *argv[], bool disable_hidpi) { Hardware::set_display_power(true); Hardware::set_brightness(65); @@ -118,13 +118,13 @@ void initApp(int argc, char *argv[]) { std::signal(SIGINT, sigTermHandler); std::signal(SIGTERM, sigTermHandler); + if (disable_hidpi) { #ifdef __APPLE__ - { // Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering QApplication tmp(argc, argv); qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio() ).toLocal8Bit()); - } #endif + } setQtSurfaceFormat(); } diff --git a/selfdrive/ui/qt/util.h b/selfdrive/ui/qt/util.h index 3188f3f9b9..5b66ec9fa4 100644 --- a/selfdrive/ui/qt/util.h +++ b/selfdrive/ui/qt/util.h @@ -19,7 +19,7 @@ void clearLayout(QLayout* layout); void setQtSurfaceFormat(); QString timeAgo(const QDateTime &date); void swagLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg); -void initApp(int argc, char *argv[]); +void initApp(int argc, char *argv[], bool disable_hidpi = true); QWidget* topWidget (QWidget* widget); QPixmap loadPixmap(const QString &fileName, const QSize &size = {}, Qt::AspectRatioMode aspectRatioMode = Qt::KeepAspectRatio); QPixmap bootstrapPixmap(const QString &id); diff --git a/selfdrive/ui/qt/widgets/cameraview.cc b/selfdrive/ui/qt/widgets/cameraview.cc index de3b64cffd..016129c348 100644 --- a/selfdrive/ui/qt/widgets/cameraview.cc +++ b/selfdrive/ui/qt/widgets/cameraview.cc @@ -114,6 +114,16 @@ CameraWidget::~CameraWidget() { doneCurrent(); } +// Qt uses device-independent pixels, depending on platform this may be +// different to what OpenGL uses +int CameraWidget::glWidth() { + return width() * devicePixelRatio(); +} + +int CameraWidget::glHeight() { + return height() * devicePixelRatio(); +} + void CameraWidget::initializeGL() { initializeOpenGLFunctions(); @@ -188,7 +198,7 @@ void CameraWidget::availableStreamsUpdated(std::set streams) { } void CameraWidget::updateFrameMat() { - int w = width(), h = height(); + int w = glWidth(), h = glHeight(); if (zoomed_view) { if (active_stream_type == VISION_STREAM_DRIVER) { @@ -266,7 +276,7 @@ void CameraWidget::paintGL() { updateFrameMat(); - glViewport(0, 0, width(), height()); + glViewport(0, 0, glWidth(), glHeight()); glBindVertexArray(frame_vao); glUseProgram(program->programId()); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); diff --git a/selfdrive/ui/qt/widgets/cameraview.h b/selfdrive/ui/qt/widgets/cameraview.h index 9135620d35..8a140e5290 100644 --- a/selfdrive/ui/qt/widgets/cameraview.h +++ b/selfdrive/ui/qt/widgets/cameraview.h @@ -54,6 +54,9 @@ protected: void vipcThread(); void clearFrames(); + int glWidth(); + int glHeight(); + bool zoomed_view; GLuint frame_vao, frame_vbo, frame_ibo; GLuint textures[2]; diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 5f3511d3cc..2e4e833e07 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -992,22 +992,10 @@ This may take up to a minute. Experimental Mode Experimenteller Modus - - Experimental openpilot Longitudinal Control - Experimenteller Openpilot Tempomat - - - WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB). - WARNUNG: Der Openpilot Tempomat ist für dieses Auto experimentell und deaktiviert den Notbremsassistenten. - Disengage on Accelerator Pedal Bei Gasbetätigung ausschalten - - On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when using experimental openpilot longitudinal control. - Bei diesem auto wird standardmäßig der im Auto eingebaute adaptive Tempomat anstelle des Openpilot Tempomats benutzt. Aktiviere diesen Schalter, um zum Openpilot Tempomaten zu wechseln. Es ist empfohlen den Experimentellen Modus bei Nutzung des Openpilot Tempomats zu aktivieren. - openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: Openpilot fährt standardmäßig im <b>entspannten Modus</b>. Der Experimentelle Modus aktiviert<b>Alpha-level Funktionen</b>, die noch nicht für den entspannten Modus bereit sind. Die experimentellen Funktionen sind die Folgenden: @@ -1044,6 +1032,18 @@ This may take up to a minute. Enable experimental longitudinal control to allow Experimental mode. Aktiviere den experimentellen Openpilot Tempomaten für experimentelle Funktionen. + + openpilot Longitudinal Control (Alpha) + + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + + + + On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + Updater diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 1e0223606b..06435d3b41 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -962,10 +962,6 @@ This may take up to a minute. Upload data from the driver facing camera and help improve the driver monitoring algorithm. 車内カメラの映像をアップロードし、ドライバー監視システムのアルゴリズムの向上に役立てます。 - - Experimental openpilot Longitudinal Control - 実験段階のopenpilotによるアクセル制御 - Disengage on Accelerator Pedal アクセルを踏むと openpilot を中断 @@ -994,14 +990,6 @@ This may take up to a minute. Experimental Mode 実験モード - - WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB). - 警告: この車種でのopenpilotによるアクセル制御は実験段階であり、衝突被害軽減ブレーキ(AEB)を無効化します。 - - - On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when using experimental openpilot longitudinal control. - openpilotはこの車の場合、車に内蔵されているACCを標準で利用します。この機能を有効にすることで実験段階のopenpilotによるアクセル制御を利用できます。実験モードと合わせて利用することをお勧めします。 - openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: openpilotは標準ではゆっくりとくつろげる運転を提供します。この実験モードを有効にすると、以下のくつろげる段階ではない開発中の機能を利用する事ができます。 @@ -1038,6 +1026,18 @@ This may take up to a minute. Enable experimental longitudinal control to allow Experimental mode. 実験段階のopenpilotによるアクセル制御を有効にしてください。 + + openpilot Longitudinal Control (Alpha) + + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + + + + On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + Updater diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index be11f2efde..b55a56a061 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -963,10 +963,6 @@ This may take up to a minute. Upload data from the driver facing camera and help improve the driver monitoring algorithm. 운전자 카메라에서 데이터를 업로드하고 운전자 모니터링 알고리즘을 개선합니다. - - Experimental openpilot Longitudinal Control - openpilot 롱컨트롤 (실험적) - Disengage on Accelerator Pedal 가속페달 조작시 해제 @@ -995,14 +991,6 @@ This may take up to a minute. Experimental Mode 실험적 모드 - - WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB). - 경고: openpilot 롱컨트롤은 실험적인 기능으로 차량의 자동긴급제동(AEB)를 비활성화합니다. - - - On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when using experimental openpilot longitudinal control. - 이 차량은 openpilot 롱컨트롤 대신 차량의 내장 ACC로 기본 설정됩니다. openpilot 롱컨트롤을 사용하려면 이 옵션을 활성화하세요. 실험적 openpilot 롱컨트롤을 사용하는 경우 실험적 모드를 활성화 하세요. - openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: openpilot은 기본적으로 <b>안정적 모드</b>로 주행합니다. 실험적 모드는 안정적 모드에 준비되지 않은 <b>알파 수준 기능</b>을 활성화 합니다. 실험적 모드의 특징은 아래에 나열되어 있습니다 @@ -1039,6 +1027,18 @@ This may take up to a minute. Enable experimental longitudinal control to allow Experimental mode. 실험적 롱컨트롤을 사용하려면 실험적 모드를 활성화 하세요. + + openpilot Longitudinal Control (Alpha) + openpilot 롱컨트롤 (알파) + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + 경고: openpilot 롱컨트롤은 알파 기능으로 차량의 자동긴급제동(AEB)를 비활성화합니다. + + + On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + 이 차량은 openpilot 롱컨트롤 대신 차량의 내장 ACC로 기본 설정됩니다. openpilot 롱컨트롤으로 전환하려면 이 기능을 활성화하세요. openpilot 롱컨트롤 알파를 활성화하는경우 실험적 모드 활성화를 권장합니다. + Updater diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 8dc3a9830d..34fbc8abf0 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -967,10 +967,6 @@ Isso pode levar até um minuto. Upload data from the driver facing camera and help improve the driver monitoring algorithm. Upload dados da câmera voltada para o motorista e ajude a melhorar o algoritmo de monitoramentor. - - Experimental openpilot Longitudinal Control - Controle longitudinal experimental openpilot - Disengage on Accelerator Pedal Desacionar com Pedal do Acelerador @@ -999,14 +995,6 @@ Isso pode levar até um minuto. Experimental Mode Modo Experimental - - WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB). - ATENÇÃO: o controle longitudinal do openpilot é experimental para este carro e desativará a Frenagem Automática de Emergência (AEB). - - - On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when using experimental openpilot longitudinal control. - Neste carro o penpilot por padrão utiliza o ACC nativo do veículo ao invés de controlar longitudinalmente. Ative isto para mudar para o controle longitudinal do openpilot. Ativar o Modo Experimental é recomendado quando em uso do controle longitudinal experimental do openpilot. - openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: openpilot por padrão funciona em <b>modo chill</b>. modo Experimental ativa <b>recursos de nível-alfa</b> que não estão prontos para o modo chill. Recursos experimentais estão listados abaixo: @@ -1043,6 +1031,18 @@ Isso pode levar até um minuto. Enable experimental longitudinal control to allow Experimental mode. Ative o controle longitudinal experimental para permitir o modo Experimental. + + openpilot Longitudinal Control (Alpha) + Controle Longitudinal openpilot (Alpha) + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + AVISO: o controle longitudinal openpilot está em alfa para este carro e desativará a Frenagem Automática de Emergência (AEB). + + + On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + Neste carro, o openpilot tem como padrão o ACC embutido do carro em vez do controle longitudinal do openpilot. Habilite isso para alternar para o controle longitudinal openpilot. Recomenda-se ativar o modo Experimental ao ativar o alfa de controle longitudinal openpilot. + Updater diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index cfc055ac98..7cacc72f1c 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -960,10 +960,6 @@ This may take up to a minute. Upload data from the driver facing camera and help improve the driver monitoring algorithm. 上传驾驶员摄像头的数据,帮助改进驾驶员监控算法。 - - Experimental openpilot Longitudinal Control - 试验性的openpilot纵向控制 - Disengage on Accelerator Pedal 踩油门时取消控制 @@ -992,14 +988,6 @@ This may take up to a minute. Experimental Mode 测试模式 - - WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB). - 警告: 此车辆的openpilot纵向控制是试验性功能,且将禁用AEB自动刹车功能。 - - - On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when using experimental openpilot longitudinal control. - 针对此车辆,openpilot默认使用车辆自带的ACC,而非openpilot的纵向控制。启用此选项将切换到openpilot纵向控制。当使用试验性的openpilot纵向控制时,建议同时启用试验模式。 - openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: openpilot 默认 <b>轻松模式</b>驾驶车辆。试验模式启用一些轻松模式之外的 <b>试验性功能</b>。试验性功能包括: @@ -1036,6 +1024,18 @@ This may take up to a minute. Enable experimental longitudinal control to allow Experimental mode. 启用试验性的纵向控制,以便允许使用试验模式。 + + openpilot Longitudinal Control (Alpha) + + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + + + + On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + Updater diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 83db0b7301..e559ca88a6 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -962,10 +962,6 @@ This may take up to a minute. Upload data from the driver facing camera and help improve the driver monitoring algorithm. 上傳駕駛監控的錄像來協助我們提升駕駛監控的準確率。 - - Experimental openpilot Longitudinal Control - 使用 openpilot 縱向控制(實驗) - Disengage on Accelerator Pedal 油門取消控車 @@ -994,14 +990,6 @@ This may take up to a minute. Experimental Mode 實驗模式 - - WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB). - 警告:openpilot 縱向控制在這輛車上仍屬實驗性質,啟用後會喪失自動緊急煞車 (AEB) 功能。 - - - On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when using experimental openpilot longitudinal control. - 在本車輛中,openpilot預設將使用原車內建的ACC系統,而非openpilot縱向控制。開啟此開關來啟用openpilot縱向控制,使用此選項時建議一併啟用實驗模式。 - openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: openpilot 預設以 <b>輕鬆模式</b> 駕駛。 實驗模式啟用了尚未準備好進入輕鬆模式的 <b>alpha 級功能</b>。實驗功能如下: @@ -1038,6 +1026,18 @@ This may take up to a minute. Enable experimental longitudinal control to allow Experimental mode. 啟用實驗性縱向控制以使用實驗模式。 + + openpilot Longitudinal Control (Alpha) + + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + + + + On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + Updater diff --git a/system/camerad/cameras/camera_common.cc b/system/camerad/cameras/camera_common.cc index 7ee3738057..9ef5d89a4e 100644 --- a/system/camerad/cameras/camera_common.cc +++ b/system/camerad/cameras/camera_common.cc @@ -157,9 +157,6 @@ void fill_frame_data(cereal::FrameData::Builder &framed, const FrameMetadata &fr framed.setHighConversionGain(frame_data.high_conversion_gain); framed.setMeasuredGreyFraction(frame_data.measured_grey_fraction); framed.setTargetGreyFraction(frame_data.target_grey_fraction); - framed.setLensPos(frame_data.lens_pos); - framed.setLensErr(frame_data.lens_err); - framed.setLensTruePos(frame_data.lens_true_pos); framed.setProcessingTime(frame_data.processing_time); const float ev = c->cur_ev[frame_data.frame_id % 3]; diff --git a/system/camerad/cameras/camera_common.h b/system/camerad/cameras/camera_common.h index 088b9f7939..07d1291a2a 100644 --- a/system/camerad/cameras/camera_common.h +++ b/system/camerad/cameras/camera_common.h @@ -66,11 +66,6 @@ typedef struct FrameMetadata { float measured_grey_fraction; float target_grey_fraction; - // Focus - unsigned int lens_pos; - float lens_err; - float lens_true_pos; - float processing_time; } FrameMetadata; diff --git a/system/camerad/cameras/camera_qcom2.cc b/system/camerad/cameras/camera_qcom2.cc index 92b3bde413..e8b8bdc77a 100644 --- a/system/camerad/cameras/camera_qcom2.cc +++ b/system/camerad/cameras/camera_qcom2.cc @@ -71,7 +71,7 @@ const int DC_GAIN_MIN_WEIGHT_OX03C10 = 1; // always on is fine const int DC_GAIN_MAX_WEIGHT_OX03C10 = 1; const float TARGET_GREY_FACTOR_AR0231 = 1.0; -const float TARGET_GREY_FACTOR_OX03C10 = 0.02; +const float TARGET_GREY_FACTOR_OX03C10 = 0.01; const float sensor_analog_gains_AR0231[] = { 1.0/8.0, 2.0/8.0, 2.0/7.0, 3.0/7.0, // 0, 1, 2, 3 @@ -101,7 +101,7 @@ const float ANALOG_GAIN_COST_LOW_AR0231 = 0.1; const float ANALOG_GAIN_COST_HIGH_AR0231 = 5.0; const int ANALOG_GAIN_MIN_IDX_OX03C10 = 0x0; -const int ANALOG_GAIN_REC_IDX_OX03C10 = 0x11; // 2.5x +const int ANALOG_GAIN_REC_IDX_OX03C10 = 0x0; // 1x const int ANALOG_GAIN_MAX_IDX_OX03C10 = 0x36; const int ANALOG_GAIN_COST_DELTA_OX03C10 = -1; const float ANALOG_GAIN_COST_LOW_OX03C10 = 0.4; @@ -1175,7 +1175,7 @@ void CameraState::set_camera_exposure(float grey_frac) { // t_HCG&t_LCG + t_VS on LPD, t_SPD on SPD uint32_t hcg_time = exposure_time; uint32_t lcg_time = hcg_time; - uint32_t spd_time = exposure_time_max + VS_TIME_MAX_OX03C10; + uint32_t spd_time = std::min(std::max((uint32_t)exposure_time, (exposure_time_max + VS_TIME_MAX_OX03C10) / 3), exposure_time_max + VS_TIME_MAX_OX03C10); uint32_t vs_time = std::min(std::max((uint32_t)exposure_time / 40, VS_TIME_MIN_OX03C10), VS_TIME_MAX_OX03C10); uint32_t real_gain = ox03c10_analog_gains_reg[new_exp_g]; diff --git a/system/camerad/cameras/real_debayer.cl b/system/camerad/cameras/real_debayer.cl index cff5ae455b..e15a873d6d 100644 --- a/system/camerad/cameras/real_debayer.cl +++ b/system/camerad/cameras/real_debayer.cl @@ -18,6 +18,9 @@ float3 color_correct(float3 rgb) { x += rgb.z * (float3)(-0.25277411, -0.05627105, 1.45875782); #endif + #if IS_OX + return -0.507089*exp(-12.54124638*x)+0.9655*powr(x,0.5)-0.472597*x+0.507089; + #else // tone mapping params const float gamma_k = 0.75; const float gamma_b = 0.125; @@ -28,6 +31,7 @@ float3 color_correct(float3 rgb) { return (x > mp) ? ((rk * (x-mp) * (1-(gamma_k*mp+gamma_b)) * (1+1/(rk*(1-mp))) / (1+rk*(x-mp))) + gamma_k*mp + gamma_b) : ((rk * (x-mp) * (gamma_k*mp+gamma_b) * (1+1/(rk*mp)) / (1-rk*(x-mp))) + gamma_k*mp + gamma_b); + #endif } float get_vignetting_s(float r) { diff --git a/system/hardware/base.py b/system/hardware/base.py index 31df1babe0..0b6ca44c3c 100644 --- a/system/hardware/base.py +++ b/system/hardware/base.py @@ -135,6 +135,9 @@ class HardwareBase(ABC): def get_networks(self): pass + def has_internal_panda(self) -> bool: + return False + def reset_internal_panda(self): pass diff --git a/system/hardware/tici/amplifier.py b/system/hardware/tici/amplifier.py index 9725cd689c..979bc757f6 100755 --- a/system/hardware/tici/amplifier.py +++ b/system/hardware/tici/amplifier.py @@ -1,5 +1,7 @@ +import time from smbus2 import SMBus from collections import namedtuple +from typing import List # https://datasheets.maximintegrated.com/en/ds/MAX98089.pdf @@ -104,31 +106,44 @@ class Amplifier: def __init__(self, debug=False): self.debug = debug - def set_config(self, config): - with SMBus(self.AMP_I2C_BUS) as bus: - if self.debug: - print(f"Setting \"{config.name}\" to {config.value}:") - - old_value = bus.read_byte_data(self.AMP_ADDRESS, config.register, force=True) - new_value = (old_value & (~config.mask)) | ((config.value << config.offset) & config.mask) - bus.write_byte_data(self.AMP_ADDRESS, config.register, new_value, force=True) - - if self.debug: - print(f" Changed {hex(config.register)}: {hex(old_value)} -> {hex(new_value)}") - - def set_global_shutdown(self, amp_disabled): - self.set_config(AmpConfig("Global shutdown", 0b0 if amp_disabled else 0b1, 0x51, 7, 0b10000000)) + def _get_shutdown_config(self, amp_disabled: bool) -> AmpConfig: + return AmpConfig("Global shutdown", 0b0 if amp_disabled else 0b1, 0x51, 7, 0b10000000) - def initialize_configuration(self, model): - self.set_global_shutdown(amp_disabled=True) - - for config in BASE_CONFIG: - self.set_config(config) - - for config in CONFIGS[model]: - self.set_config(config) - - self.set_global_shutdown(amp_disabled=False) + def _set_configs(self, configs: List[AmpConfig]) -> None: + with SMBus(self.AMP_I2C_BUS) as bus: + for config in configs: + if self.debug: + print(f"Setting \"{config.name}\" to {config.value}:") + + old_value = bus.read_byte_data(self.AMP_ADDRESS, config.register, force=True) + new_value = (old_value & (~config.mask)) | ((config.value << config.offset) & config.mask) + bus.write_byte_data(self.AMP_ADDRESS, config.register, new_value, force=True) + + if self.debug: + print(f" Changed {hex(config.register)}: {hex(old_value)} -> {hex(new_value)}") + + def set_configs(self, configs: List[AmpConfig]) -> bool: + # retry in case panda is using the amp + for _ in range(10): + try: + self._set_configs(configs) + return True + except OSError: + print("Failed to set amp config, retrying...") + time.sleep(0.02) + return False + + def set_global_shutdown(self, amp_disabled: bool) -> bool: + return self.set_configs([self._get_shutdown_config(amp_disabled), ]) + + def initialize_configuration(self, model: str) -> bool: + cfgs = [ + self._get_shutdown_config(True), + *BASE_CONFIG, + *CONFIGS[model], + self._get_shutdown_config(False), + ] + return self.set_configs(cfgs) if __name__ == "__main__": diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py index 9c1cc930c1..ee1d5f5279 100644 --- a/system/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -8,7 +8,7 @@ from functools import cached_property from pathlib import Path from cereal import log -from common.gpio import gpio_set, gpio_init +from common.gpio import gpio_set, gpio_init, get_irq_for_action from system.hardware.base import HardwareBase, ThermalConfig from system.hardware.tici import iwlist from system.hardware.tici.pins import GPIO @@ -63,8 +63,14 @@ MM_MODEM_ACCESS_TECHNOLOGY_LTE = 1 << 14 def sudo_write(val, path): os.system(f"sudo su -c 'echo {val} > {path}'") -def affine_irq(val, irq): - sudo_write(str(val), f"/proc/irq/{irq}/smp_affinity_list") + +def affine_irq(val, action): + irq = get_irq_for_action(action) + if len(irq) == 0: + print(f"No IRQs found for '{action}'") + return + for i in irq: + sudo_write(str(val), f"/proc/irq/{i}/smp_affinity_list") class Tici(HardwareBase): @@ -432,12 +438,20 @@ class Tici(HardwareBase): sudo_write(gov, f'/sys/devices/system/cpu/cpufreq/policy{n}/scaling_governor') # *** IRQ config *** - affine_irq(5, 565) # kgsl-3d0 - affine_irq(4, 126) # SPI goes on boardd core - affine_irq(4, 740) # xhci-hcd:usb1 goes on the boardd core - affine_irq(4, 1069) # xhci-hcd:usb3 goes on the boardd core - for irq in range(237, 246): - affine_irq(5, irq) # camerad + + # GPU + affine_irq(5, "kgsl-3d0") + + # boardd core + affine_irq(4, "spi_geni") # SPI + affine_irq(4, "xhci-hcd:usb3") # aux panda USB (or potentially anything else on USB) + if "tici" in self.model: + affine_irq(4, "xhci-hcd:usb1") # internal panda USB + + # camerad core + camera_irqs = ("cci", "cpas_camnoc", "cpas-cdm", "csid", "ife", "csid", "csid-lite", "ife-lite") + for n in camera_irqs: + affine_irq(5, n) def get_gpu_usage_percent(self): try: @@ -452,12 +466,20 @@ class Tici(HardwareBase): # Allow thermald to write engagement status to kmsg os.system("sudo chmod a+w /dev/kmsg") + # TODO: remove the if once agnos 7 ships + # Ensure fan gpio is enabled so fan runs until shutdown, also turned on at boot by the ABL + if os.path.exists('/sys/class/gpio/gpio49/'): + gpio_init(GPIO.SOM_ST_IO, True) + gpio_set(GPIO.SOM_ST_IO, 1) + # *** IRQ config *** # move these off the default core - affine_irq(1, 7) # msm_drm - affine_irq(1, 250) # msm_vidc - affine_irq(1, 8) # i2c_geni (sensord) + affine_irq(1, "msm_drm") + affine_irq(1, "msm_vidc") + affine_irq(1, "i2c_geni") + + # mask off big cluster from default affinity sudo_write("f", "/proc/irq/default_smp_affinity") # *** GPU config *** @@ -543,6 +565,9 @@ class Tici(HardwareBase): except Exception: return -1, -1 + def has_internal_panda(self): + return True + def reset_internal_panda(self): gpio_init(GPIO.STM_RST_N, True) diff --git a/system/hardware/tici/pins.py b/system/hardware/tici/pins.py index fe31b9311d..082a402f15 100644 --- a/system/hardware/tici/pins.py +++ b/system/hardware/tici/pins.py @@ -10,6 +10,8 @@ class GPIO: STM_RST_N = 124 STM_BOOT0 = 134 + SOM_ST_IO = 49 + LTE_RST_N = 50 LTE_PWRKEY = 116 LTE_BOOT = 52 diff --git a/system/hardware/tici/test_agnos_updater.py b/system/hardware/tici/tests/test_agnos_updater.py similarity index 80% rename from system/hardware/tici/test_agnos_updater.py rename to system/hardware/tici/tests/test_agnos_updater.py index e0d6ed8814..86bc78881e 100755 --- a/system/hardware/tici/test_agnos_updater.py +++ b/system/hardware/tici/tests/test_agnos_updater.py @@ -4,8 +4,8 @@ import os import unittest import requests -AGNOS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__))) -MANIFEST = os.path.join(AGNOS_DIR, "agnos.json") +TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__))) +MANIFEST = os.path.join(TEST_DIR, "../agnos.json") class TestAgnosUpdater(unittest.TestCase): diff --git a/system/hardware/tici/tests/test_amplifier.py b/system/hardware/tici/tests/test_amplifier.py new file mode 100755 index 0000000000..1cad6cfd6b --- /dev/null +++ b/system/hardware/tici/tests/test_amplifier.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +import time +import random +import unittest +import subprocess + +from panda import Panda +from system.hardware import TICI +from system.hardware.tici.hardware import Tici +from system.hardware.tici.amplifier import Amplifier + + +class TestAmplifier(unittest.TestCase): + + @classmethod + def setUpClass(cls): + if not TICI: + raise unittest.SkipTest + + def setUp(self): + # clear dmesg + subprocess.check_call("sudo dmesg -C", shell=True) + + self.panda = Panda() + self.panda.reset() + + def tearDown(self): + self.panda.reset(reconnect=False) + + def _check_for_i2c_errors(self, expected): + dmesg = subprocess.check_output("dmesg", shell=True, encoding='utf8') + i2c_lines = [l for l in dmesg.strip().splitlines() if 'i2c_geni a88000.i2c' in l] + i2c_str = '\n'.join(i2c_lines) + if not expected: + assert len(i2c_lines) == 0 + else: + assert "i2c error :-107" in i2c_str or "Bus arbitration lost" in i2c_str + + def test_init(self): + amp = Amplifier(debug=True) + r = amp.initialize_configuration(Tici().model) + assert r + self._check_for_i2c_errors(False) + + def test_shutdown(self): + amp = Amplifier(debug=True) + for _ in range(10): + r = amp.set_global_shutdown(True) + r = amp.set_global_shutdown(False) + assert r + self._check_for_i2c_errors(False) + + def test_init_while_siren_play(self): + for _ in range(5): + self.panda.set_siren(False) + time.sleep(0.1) + + self.panda.set_siren(True) + time.sleep(random.randint(0, 5)) + + amp = Amplifier(debug=True) + r = amp.initialize_configuration(Tici().model) + assert r + + # make sure we're a good test + self._check_for_i2c_errors(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/system/hardware/tici/test_power_draw.py b/system/hardware/tici/tests/test_power_draw.py similarity index 100% rename from system/hardware/tici/test_power_draw.py rename to system/hardware/tici/tests/test_power_draw.py diff --git a/system/loggerd/bootlog.cc b/system/loggerd/bootlog.cc index 4deb5b41f3..becd293c02 100644 --- a/system/loggerd/bootlog.cc +++ b/system/loggerd/bootlog.cc @@ -2,6 +2,7 @@ #include #include "cereal/messaging/messaging.h" +#include "common/params.h" #include "common/swaglog.h" #include "system/loggerd/logger.h" @@ -48,7 +49,8 @@ static kj::Array build_boot_log() { } int main(int argc, char** argv) { - const std::string path = LOG_ROOT + "/boot/" + logger_get_route_name(); + const std::string timestr = logger_get_route_name(); + const std::string path = LOG_ROOT + "/boot/" + timestr; LOGW("bootlog to %s", path.c_str()); // Open bootlog @@ -61,5 +63,8 @@ int main(int argc, char** argv) { // Write bootlog file.write(build_boot_log().asBytes()); + // Write out bootlog param to match routes with bootlog + Params().put("CurrentBootlog", timestr.c_str()); + return 0; } diff --git a/system/loggerd/uploader.py b/system/loggerd/uploader.py index d4ed6dca28..13d17b58c5 100644 --- a/system/loggerd/uploader.py +++ b/system/loggerd/uploader.py @@ -9,6 +9,7 @@ import threading import time import traceback from pathlib import Path +from typing import BinaryIO, Iterator, List, Optional, Tuple, Union from cereal import log import cereal.messaging as messaging @@ -31,10 +32,17 @@ force_wifi = os.getenv("FORCEWIFI") is not None fake_upload = os.getenv("FAKEUPLOAD") is not None -def get_directory_sort(d): +class FakeResponse: + def __init__(self): + self.status_code = 200 + + +UploadResponse = Union[requests.Response, FakeResponse] + +def get_directory_sort(d: str) -> List[str]: return list(map(lambda s: s.rjust(10, '0'), d.rsplit('--', 1))) -def listdir_by_creation(d): +def listdir_by_creation(d: str) -> List[str]: try: paths = os.listdir(d) paths = sorted(paths, key=get_directory_sort) @@ -43,7 +51,7 @@ def listdir_by_creation(d): cloudlog.exception("listdir_by_creation failed") return list() -def clear_locks(root): +def clear_locks(root: str) -> None: for logname in os.listdir(root): path = os.path.join(root, logname) try: @@ -54,16 +62,14 @@ def clear_locks(root): cloudlog.exception("clear_locks failed") -class Uploader(): - def __init__(self, dongle_id, root): +class Uploader: + def __init__(self, dongle_id: str, root: str): self.dongle_id = dongle_id self.api = Api(dongle_id) self.root = root - self.upload_thread = None - - self.last_resp = None - self.last_exc = None + self.last_resp: Optional[UploadResponse] = None + self.last_exc: Optional[Tuple[Exception, str]] = None self.immediate_size = 0 self.immediate_count = 0 @@ -76,12 +82,12 @@ class Uploader(): self.immediate_folders = ["crash/", "boot/"] self.immediate_priority = {"qlog": 0, "qlog.bz2": 0, "qcamera.ts": 1} - def get_upload_sort(self, name): + def get_upload_sort(self, name: str) -> int: if name in self.immediate_priority: return self.immediate_priority[name] return 1000 - def list_upload_files(self): + def list_upload_files(self) -> Iterator[Tuple[str, str, str]]: if not os.path.isdir(self.root): return @@ -103,7 +109,7 @@ class Uploader(): fn = os.path.join(path, name) # skip files already uploaded try: - is_uploaded = getxattr(fn, UPLOAD_ATTR_NAME) + is_uploaded = bool(getxattr(fn, UPLOAD_ATTR_NAME)) except OSError: cloudlog.event("uploader_getxattr_failed", exc=self.last_exc, key=key, fn=fn) is_uploaded = True # deleter could have deleted @@ -117,22 +123,22 @@ class Uploader(): except OSError: pass - yield (name, key, fn) + yield name, key, fn - def next_file_to_upload(self): + def next_file_to_upload(self) -> Optional[Tuple[str, str, str]]: upload_files = list(self.list_upload_files()) for name, key, fn in upload_files: if any(f in fn for f in self.immediate_folders): - return (name, key, fn) + return name, key, fn for name, key, fn in upload_files: if name in self.immediate_priority: - return (name, key, fn) + return name, key, fn return None - def do_upload(self, key, fn): + def do_upload(self, key: str, fn: str) -> None: try: url_resp = self.api.get("v1.4/" + self.dongle_id + "/upload_url/", timeout=10, path=key, access_token=self.api.get_token()) if url_resp.status_code == 412: @@ -146,17 +152,13 @@ class Uploader(): if fake_upload: cloudlog.debug(f"*** WARNING, THIS IS A FAKE UPLOAD TO {url} ***") - - class FakeResponse(): - def __init__(self): - self.status_code = 200 - self.last_resp = FakeResponse() else: with open(fn, "rb") as f: + data: BinaryIO if key.endswith('.bz2') and not fn.endswith('.bz2'): - data = bz2.compress(f.read()) - data = io.BytesIO(data) + compressed = bz2.compress(f.read()) + data = io.BytesIO(compressed) else: data = f @@ -165,7 +167,7 @@ class Uploader(): self.last_exc = (e, traceback.format_exc()) raise - def normal_upload(self, key, fn): + def normal_upload(self, key: str, fn: str) -> Optional[UploadResponse]: self.last_resp = None self.last_exc = None @@ -176,7 +178,7 @@ class Uploader(): return self.last_resp - def upload(self, name, key, fn, network_type, metered): + def upload(self, name: str, key: str, fn: str, network_type: int, metered: bool) -> bool: try: sz = os.path.getsize(fn) except OSError: @@ -224,7 +226,7 @@ class Uploader(): return msg -def uploader_fn(exit_event): +def uploader_fn(exit_event: threading.Event) -> None: try: set_core_affinity([0, 1, 2, 3]) except Exception: @@ -279,7 +281,7 @@ def uploader_fn(exit_event): pm.send("uploaderState", uploader.get_msg()) -def main(): +def main() -> None: uploader_fn(threading.Event()) diff --git a/system/sensord/rawgps/rawgpsd.py b/system/sensord/rawgps/rawgpsd.py index f75ceee7ed..8f243af9a5 100755 --- a/system/sensord/rawgps/rawgpsd.py +++ b/system/sensord/rawgps/rawgpsd.py @@ -270,7 +270,6 @@ def main() -> NoReturn: msg = messaging.new_message('gpsLocation') gps = msg.gpsLocation - gps.flags = 1 gps.latitude = report["t_DblFinalPosLatLon[0]"] * 180/math.pi gps.longitude = report["t_DblFinalPosLatLon[1]"] * 180/math.pi gps.altitude = report["q_FltFinalPosAlt"] @@ -283,6 +282,8 @@ def main() -> NoReturn: gps.verticalAccuracy = report["q_FltVdop"] gps.bearingAccuracyDeg = report["q_FltHeadingUncRad"] * 180/math.pi gps.speedAccuracy = math.sqrt(sum([x**2 for x in vNEDsigma])) + # quectel gps verticalAccuracy is clipped to 500, set invalid if so + gps.flags = 1 if gps.verticalAccuracy != 500 else 0 pm.send('gpsLocation', msg) diff --git a/system/sensord/tests/test_sensord.py b/system/sensord/tests/test_sensord.py index c6fe33129a..6e8dda79bb 100755 --- a/system/sensord/tests/test_sensord.py +++ b/system/sensord/tests/test_sensord.py @@ -7,7 +7,8 @@ from collections import namedtuple, defaultdict import cereal.messaging as messaging from cereal import log -from system.hardware import TICI, HARDWARE +from common.gpio import get_irq_for_action +from system.hardware import TICI from selfdrive.manager.process_config import managed_processes BMX = { @@ -70,7 +71,6 @@ ALL_SENSORS = { } } -LSM_IRQ = 336 def get_irq_count(irq: int): with open(f"/sys/kernel/irq/{irq}/per_cpu_count") as f: @@ -101,9 +101,6 @@ class TestSensord(unittest.TestCase): if not TICI: raise unittest.SkipTest - # make sure gpiochip0 is readable - HARDWARE.initialize_hardware() - # enable LSM self test os.environ["LSM_SELF_TEST"] = "1" @@ -114,6 +111,9 @@ class TestSensord(unittest.TestCase): time.sleep(3) cls.sample_secs = 10 cls.events = read_sensor_events(cls.sample_secs) + + # determine sensord's irq + cls.sensord_irq = get_irq_for_action("sensord")[0] finally: # teardown won't run if this doesn't succeed managed_processes["sensord"].stop() @@ -121,8 +121,6 @@ class TestSensord(unittest.TestCase): @classmethod def tearDownClass(cls): managed_processes["sensord"].stop() - if "LSM_SELF_TEST" in os.environ: - del os.environ['LSM_SELF_TEST'] def tearDown(self): managed_processes["sensord"].stop() @@ -250,9 +248,9 @@ class TestSensord(unittest.TestCase): time.sleep(3) # read /proc/interrupts to verify interrupts are received - state_one = get_irq_count(LSM_IRQ) + state_one = get_irq_count(self.sensord_irq) time.sleep(1) - state_two = get_irq_count(LSM_IRQ) + state_two = get_irq_count(self.sensord_irq) error_msg = f"no interrupts received after sensord start!\n{state_one} {state_two}" assert state_one != state_two, error_msg @@ -261,9 +259,9 @@ class TestSensord(unittest.TestCase): time.sleep(1) # read /proc/interrupts to verify no more interrupts are received - state_one = get_irq_count(LSM_IRQ) + state_one = get_irq_count(self.sensord_irq) time.sleep(1) - state_two = get_irq_count(LSM_IRQ) + state_two = get_irq_count(self.sensord_irq) assert state_one == state_two, "Interrupts received after sensord stop!" diff --git a/tools/README.md b/tools/README.md index 3969deb24d..c3f5b0ada5 100644 --- a/tools/README.md +++ b/tools/README.md @@ -53,6 +53,7 @@ Learn about the openpilot ecosystem and tools by playing our [CTF](/tools/CTF.md ``` ├── ubuntu_setup.sh # Setup script for Ubuntu ├── mac_setup.sh # Setup script for macOS +├── cabana/ # View and plot CAN messages from drives or in realtime ├── joystick/ # Control your car with a joystick ├── lib/ # Libraries to support the tools and reading openpilot logs ├── plotjuggler/ # A tool to plot openpilot logs diff --git a/tools/cabana/.gitignore b/tools/cabana/.gitignore index 73879ab05d..c3f5ef2b69 100644 --- a/tools/cabana/.gitignore +++ b/tools/cabana/.gitignore @@ -1,7 +1,7 @@ moc_* *.moc -_cabana +cabana settings -car_fingerprint_to_dbc.json -tests/_test_cabana +dbc/car_fingerprint_to_dbc.json +tests/test_cabana diff --git a/tools/cabana/README.md b/tools/cabana/README.md index db247c39c5..921decff3c 100644 --- a/tools/cabana/README.md +++ b/tools/cabana/README.md @@ -8,22 +8,26 @@ Cabana is a tool developed to view raw CAN data. One use for this is creating an ```bash $ ./cabana -h -Usage: ./_cabana [options] route +Usage: ./cabana [options] route Options: - -h, --help Displays this help. - --demo use a demo route instead of providing your own - --qcam load qcamera - --ecam load wide road camera - --stream read can messages from live streaming - --zmq the ip address on which to receive zmq messages - --data_dir local directory with routes - --no-vipc do not output video - --dbc dbc file to open + -h, --help Displays help on commandline options. + --help-all Displays help including Qt specific options. + --demo use a demo route instead of providing your own + --qcam load qcamera + --ecam load wide road camera + --stream read can messages from live streaming + --panda read can messages from panda + --panda-serial read can messages from panda with given serial + --zmq the ip address on which to receive zmq + messages + --data_dir local directory with routes + --no-vipc do not output video + --dbc dbc file to open Arguments: - route the drive to replay. find your drives at - connect.comma.ai + route the drive to replay. find your drives at + connect.comma.ai ``` See [openpilot wiki](https://github.com/commaai/openpilot/wiki/Cabana) diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index 3cb5913c01..a735dc438e 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -1,6 +1,6 @@ import os Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', 'replay_lib', - 'cereal', 'transformations', 'widgets', 'opendbc') + 'cereal', 'transformations', 'widgets') base_frameworks = qt_env['FRAMEWORKS'] base_libs = [common, messaging, cereal, visionipc, transformations, 'zmq', @@ -15,8 +15,9 @@ else: qt_libs = ['qt_util'] + base_libs -cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, opendbc,'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv'] + qt_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 opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../opendbc").abspath) cabana_env['CXXFLAGS'] += [opendbc_path] @@ -28,16 +29,15 @@ cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, assets_src, "asset prev_moc_path = cabana_env['QT_MOCHPREFIX'] cabana_env['QT_MOCHPREFIX'] = os.path.dirname(prev_moc_path) + '/cabana/moc_' -cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'chartswidget.cc', 'historylog.cc', 'videowidget.cc', 'signaledit.cc', 'dbc.cc', 'dbcmanager.cc', - 'commands.cc', 'messageswidget.cc', 'route.cc', 'settings.cc', 'util.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) -cabana_env.Program('_cabana', ['cabana.cc', cabana_lib, assets], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) - -if arch == "Darwin": - cabana_env.Execute('install_name_tool -change opendbc/can/libdbc.dylib @loader_path/../../opendbc/can/libdbc.dylib ./_cabana') +cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', + 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', + 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', + 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'util.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) +cabana_env.Program('cabana', ['cabana.cc', cabana_lib, assets], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) if GetOption('test'): - cabana_env.Program('tests/_test_cabana', ['tests/test_runner.cc', 'tests/test_cabana.cc', cabana_lib], LIBS=[cabana_libs]) + cabana_env.Program('tests/test_cabana', ['tests/test_runner.cc', 'tests/test_cabana.cc', cabana_lib], LIBS=[cabana_libs]) def generate_dbc_json(target, source, env): - env.Execute('tools/cabana/generate_dbc_json.py --out tools/cabana/car_fingerprint_to_dbc.json') + env.Execute('tools/cabana/dbc/generate_dbc_json.py --out tools/cabana/dbc/car_fingerprint_to_dbc.json') cabana_env.Command('generate_dbc_json', [], generate_dbc_json) diff --git a/tools/cabana/binaryview.cc b/tools/cabana/binaryview.cc index 0a4f6fc999..3a03ee0b70 100644 --- a/tools/cabana/binaryview.cc +++ b/tools/cabana/binaryview.cc @@ -11,7 +11,7 @@ #include #include "tools/cabana/commands.h" -#include "tools/cabana/signaledit.h" +#include "tools/cabana/signalview.h" // BinaryView @@ -239,7 +239,19 @@ std::tuple BinaryView::getSelection(QModelIndex index) { if (index.column() == 8) { index = model->index(index.row(), 7); } - bool is_lb = (resize_sig && resize_sig->is_little_endian) || (!resize_sig && index < anchor_index); + bool is_lb = true; + if (resize_sig) { + is_lb = resize_sig->is_little_endian; + } else if (settings.drag_direction == Settings::DragDirection::MsbFirst) { + is_lb = index < anchor_index; + } else if (settings.drag_direction == Settings::DragDirection::LsbFirst) { + is_lb = !(index < anchor_index); + } else if (settings.drag_direction == Settings::DragDirection::AlwaysLE) { + is_lb = true; + } else if (settings.drag_direction == Settings::DragDirection::AlwaysBE) { + is_lb = false; + } + int cur_bit_idx = get_bit_index(index, is_lb); int anchor_bit_idx = get_bit_index(anchor_index, is_lb); auto [start_bit, end_bit] = std::minmax(cur_bit_idx, anchor_bit_idx); @@ -273,15 +285,27 @@ 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); + for (int i = 0; i < valid_rows * column_count; ++i) { + items[i].valid = true; + } endResetModel(); updateState(); } +void BinaryViewModel::updateItem(int row, int col, const QString &val, const QColor &color) { + auto &item = items[row * column_count + col]; + if (item.val != val || item.bg_color != color) { + item.val = val; + item.bg_color = color; + auto idx = index(row, col); + emit dataChanged(idx, idx, {Qt::DisplayRole}); + } +} + void BinaryViewModel::updateState() { - auto prev_items = items; const auto &last_msg = can->lastMessage(msg_id); const auto &binary = last_msg.dat; - // data size may changed. if (binary.size() > row_count) { beginInsertRows({}, row_count, binary.size() - 1); @@ -290,32 +314,23 @@ void BinaryViewModel::updateState() { endInsertRows(); } - double max_f = 255.0; - double factor = 0.25; - double scaler = max_f / log2(1.0 + factor); + const double max_f = 255.0; + const double factor = 0.25; + const double scaler = max_f / log2(1.0 + factor); for (int i = 0; i < binary.size(); ++i) { for (int j = 0; j < 8; ++j) { auto &item = items[i * column_count + j]; - item.val = ((binary[i] >> (7 - j)) & 1) != 0 ? '1' : '0'; + QString 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]; 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); - item.bg_color.setAlpha(alpha); - } - items[i * column_count + 8].val = toHex(binary[i]); - items[i * column_count + 8].bg_color = last_msg.colors[i]; - } - for (int i = binary.size() * column_count; i < items.size(); ++i) { - items[i].val = "-"; - } - - for (int i = 0; i < items.size(); ++i) { - if (i >= prev_items.size() || prev_items[i].val != items[i].val || prev_items[i].bg_color != items[i].bg_color) { - auto idx = index(i / column_count, i % column_count); - emit dataChanged(idx, idx); + auto color = item.bg_color; + color.setAlpha(alpha); + updateItem(i, j, val, color); } + updateItem(i, 8, toHex(binary[i]), last_msg.colors[i]); } } @@ -376,6 +391,9 @@ void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op } } + if (!item->valid) { + painter->fillRect(option.rect, QBrush(Qt::darkGray, Qt::BDiagPattern)); + } painter->drawText(option.rect, Qt::AlignCenter, item->val); if (item->is_msb || item->is_lsb) { painter->setFont(small_font); diff --git a/tools/cabana/binaryview.h b/tools/cabana/binaryview.h index f677d11941..f80b4520ed 100644 --- a/tools/cabana/binaryview.h +++ b/tools/cabana/binaryview.h @@ -6,7 +6,7 @@ #include #include -#include "tools/cabana/dbcmanager.h" +#include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" class BinaryItemDelegate : public QStyledItemDelegate { @@ -26,6 +26,7 @@ public: BinaryViewModel(QObject *parent) : QAbstractTableModel(parent) {} void refresh(); void updateState(); + void updateItem(int row, int col, const QString &val, const QColor &color); QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const { return {}; } int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; } @@ -42,8 +43,9 @@ public: QColor bg_color = QColor(102, 86, 169, 0); bool is_msb = false; bool is_lsb = false; - QString val = "-"; + QString val; QList sigs; + bool valid = false; }; std::vector items; diff --git a/tools/cabana/cabana b/tools/cabana/cabana deleted file mode 100755 index 14647b6a10..0000000000 --- a/tools/cabana/cabana +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -cd "$(dirname "$0")" -export LD_LIBRARY_PATH="../../opendbc/can:$LD_LIBRARY_PATH" -exec ./_cabana "$@" diff --git a/tools/cabana/cabana.cc b/tools/cabana/cabana.cc index 5c182f435f..6e354da315 100644 --- a/tools/cabana/cabana.cc +++ b/tools/cabana/cabana.cc @@ -4,17 +4,19 @@ #include "common/prefix.h" #include "selfdrive/ui/qt/util.h" #include "tools/cabana/mainwin.h" -#include "tools/cabana/route.h" -#include "tools/cabana/streams/livestream.h" +#include "tools/cabana/streamselector.h" +#include "tools/cabana/streams/devicestream.h" +#include "tools/cabana/streams/pandastream.h" #include "tools/cabana/streams/replaystream.h" int main(int argc, char *argv[]) { QCoreApplication::setApplicationName("Cabana"); QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); - initApp(argc, argv); + initApp(argc, argv, false); QApplication app(argc, argv); app.setApplicationDisplayName("Cabana"); app.setWindowIcon(QIcon(":cabana-icon.png")); + utils::setTheme(settings.theme); QCommandLineParser cmd_parser; cmd_parser.addHelpOption(); @@ -23,22 +25,28 @@ int main(int argc, char *argv[]) { cmd_parser.addOption({"qcam", "load qcamera"}); cmd_parser.addOption({"ecam", "load wide road 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"}); cmd_parser.addOption({"zmq", "the ip address on which to receive zmq messages", "zmq"}); cmd_parser.addOption({"data_dir", "local directory with routes", "data_dir"}); cmd_parser.addOption({"no-vipc", "do not output video"}); cmd_parser.addOption({"dbc", "dbc file to open", "dbc"}); cmd_parser.process(app); + QString dbc_file = cmd_parser.isSet("dbc") ? cmd_parser.value("dbc") : ""; + std::unique_ptr op_prefix; std::unique_ptr stream; if (cmd_parser.isSet("stream")) { - stream.reset(new LiveStream(&app, cmd_parser.value("zmq"))); + stream.reset(new DeviceStream(&app, cmd_parser.value("zmq"))); + } else if (cmd_parser.isSet("panda") || cmd_parser.isSet("panda-serial")) { + PandaStreamConfig config = {}; + if (cmd_parser.isSet("panda-serial")) { + config.serial = cmd_parser.value("panda-serial"); + } + stream.reset(new PandaStream(&app, config)); } else { - // TODO: Remove when OpenpilotPrefix supports ZMQ -#ifndef __APPLE__ - op_prefix.reset(new OpenpilotPrefix()); -#endif uint32_t replay_flags = REPLAY_FLAG_NONE; if (cmd_parser.isSet("ecam")) { replay_flags |= REPLAY_FLAG_ECAM; @@ -56,22 +64,35 @@ int main(int argc, char *argv[]) { route = DEMO_ROUTE; } - auto replay_stream = new ReplayStream(replay_flags, &app); - stream.reset(replay_stream); if (route.isEmpty()) { - if (OpenRouteDialog dlg(nullptr); !dlg.exec()) { + AbstractStream *out_stream = nullptr; + StreamSelector dlg; + dlg.addStreamWidget(ReplayStream::widget(&out_stream)); + dlg.addStreamWidget(PandaStream::widget(&out_stream)); + dlg.addStreamWidget(DeviceStream::widget(&out_stream)); + if (!dlg.exec()) { + return 0; + } + dbc_file = dlg.dbcFile(); + stream.reset(out_stream); + } else { + // TODO: Remove when OpenpilotPrefix supports ZMQ +#ifndef __APPLE__ + op_prefix.reset(new OpenpilotPrefix()); +#endif + auto replay_stream = new ReplayStream(&app); + stream.reset(replay_stream); + if (!replay_stream->loadRoute(route, cmd_parser.value("data_dir"), replay_flags)) { return 0; } - } else if (!replay_stream->loadRoute(route, cmd_parser.value("data_dir"))) { - return 0; } } MainWindow w; // Load DBC - if (cmd_parser.isSet("dbc")) { - w.loadFile(cmd_parser.value("dbc")); + if (!dbc_file.isEmpty()) { + w.loadFile(dbc_file); } w.show(); diff --git a/tools/cabana/chart/chart.cc b/tools/cabana/chart/chart.cc new file mode 100644 index 0000000000..bd05107297 --- /dev/null +++ b/tools/cabana/chart/chart.cc @@ -0,0 +1,765 @@ +#include "tools/cabana/chart/chart.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tools/cabana/chart/chartswidget.h" + +static inline bool xLessThan(const QPointF &p, float x) { return p.x() < x; } + +ChartView::ChartView(const std::pair &x_range, ChartsWidget *parent) : charts_widget(parent), tip_label(this), QChartView(nullptr, parent) { + series_type = (SeriesType)settings.chart_series_type; + QChart *chart = new QChart(); + 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}); + + axis_x->setRange(x_range.first, x_range.second); + setChart(chart); + + createToolButtons(); + // TODO: enable zoomIn/seekTo in live streaming mode. + setRubberBand(QChartView::HorizontalRubberBand); + setMouseTracking(true); + setTheme(settings.theme == DARK_THEME ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight); + + QObject::connect(axis_y, &QValueAxis::rangeChanged, [this]() { resetChartCache(); }); + QObject::connect(axis_y, &QAbstractAxis::titleTextChanged, [this]() { resetChartCache(); }); + QObject::connect(window()->windowHandle(), &QWindow::screenChanged, [this]() { resetChartCache(); }); + + QObject::connect(dbc(), &DBCManager::signalRemoved, this, &ChartView::signalRemoved); + QObject::connect(dbc(), &DBCManager::signalUpdated, this, &ChartView::signalUpdated); + QObject::connect(dbc(), &DBCManager::msgRemoved, this, &ChartView::msgRemoved); + QObject::connect(dbc(), &DBCManager::msgUpdated, this, &ChartView::msgUpdated); +} + +void ChartView::createToolButtons() { + move_icon = new QGraphicsPixmapItem(utils::icon("grip-horizontal"), chart()); + move_icon->setToolTip(tr("Drag and drop to move chart")); + + QToolButton *remove_btn = new ToolButton("x", tr("Remove Chart")); + close_btn_proxy = new QGraphicsProxyWidget(chart()); + close_btn_proxy->setWidget(remove_btn); + close_btn_proxy->setZValue(chart()->zValue() + 11); + + // series types + QMenu *menu = new QMenu(this); + auto change_series_group = new QActionGroup(menu); + change_series_group->setExclusive(true); + QStringList types{tr("Line"), tr("Step Line"), tr("Scatter")}; + for (int i = 0; i < types.size(); ++i) { + QAction *act = new QAction(types[i], change_series_group); + act->setData(i); + act->setCheckable(true); + act->setChecked(i == (int)series_type); + menu->addAction(act); + } + menu->addSeparator(); + menu->addAction(tr("Manage Signals"), this, &ChartView::manageSignals); + split_chart_act = menu->addAction(tr("Split Chart"), [this]() { charts_widget->splitChart(this); }); + + QToolButton *manage_btn = new ToolButton("list", ""); + manage_btn->setMenu(menu); + manage_btn->setPopupMode(QToolButton::InstantPopup); + manage_btn->setStyleSheet("QToolButton::menu-indicator { image: none; }"); + manage_btn_proxy = new QGraphicsProxyWidget(chart()); + manage_btn_proxy->setWidget(manage_btn); + manage_btn_proxy->setZValue(chart()->zValue() + 11); + + QObject::connect(remove_btn, &QToolButton::clicked, [this]() { charts_widget->removeChart(this); }); + QObject::connect(change_series_group, &QActionGroup::triggered, [this](QAction *action) { + setSeriesType((SeriesType)action->data().toInt()); + }); +} + +QSize ChartView::sizeHint() const { + return {CHART_MIN_WIDTH, settings.chart_height}; +} + +void ChartView::setTheme(QChart::ChartTheme theme) { + chart()->setTheme(theme); + if (theme == QChart::ChartThemeDark) { + axis_x->setTitleBrush(palette().text()); + axis_x->setLabelsBrush(palette().text()); + axis_y->setTitleBrush(palette().text()); + axis_y->setLabelsBrush(palette().text()); + chart()->legend()->setLabelColor(palette().color(QPalette::Text)); + } + for (auto &s : sigs) { + s.series->setColor(getColor(s.sig)); + } +} + +void ChartView::addSignal(const MessageId &msg_id, const cabana::Signal *sig) { + if (hasSignal(msg_id, sig)) return; + + QXYSeries *series = createSeries(series_type, getColor(sig)); + sigs.push_back({.msg_id = msg_id, .sig = sig, .series = series}); + updateSeries(sig); + updateSeriesPoints(); + updateTitle(); + emit charts_widget->seriesChanged(); +} + +bool ChartView::hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const { + return std::any_of(sigs.begin(), sigs.end(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; }); +} + +void ChartView::removeIf(std::function predicate) { + int prev_size = sigs.size(); + for (auto it = sigs.begin(); it != sigs.end(); /**/) { + if (predicate(*it)) { + chart()->removeSeries(it->series); + it->series->deleteLater(); + it = sigs.erase(it); + } else { + ++it; + } + } + if (sigs.empty()) { + charts_widget->removeChart(this); + } else if (sigs.size() != prev_size) { + emit charts_widget->seriesChanged(); + updateAxisY(); + resetChartCache(); + } +} + +void ChartView::signalUpdated(const cabana::Signal *sig) { + if (std::any_of(sigs.begin(), sigs.end(), [=](auto &s) { return s.sig == sig; })) { + updateTitle(); + updateSeries(sig); + } +} + +void ChartView::msgUpdated(MessageId id) { + if (std::any_of(sigs.begin(), sigs.end(), [=](auto &s) { return s.msg_id == id; })) + updateTitle(); +} + +void ChartView::manageSignals() { + SignalSelector dlg(tr("Mange Chart"), this); + for (auto &s : sigs) { + dlg.addSelected(s.msg_id, s.sig); + } + if (dlg.exec() == QDialog::Accepted) { + auto items = dlg.seletedItems(); + for (auto s : items) { + addSignal(s->msg_id, s->sig); + } + removeIf([&](auto &s) { + return std::none_of(items.cbegin(), items.cend(), [&](auto &it) { return s.msg_id == it->msg_id && s.sig == it->sig; }); + }); + } +} + +void ChartView::resizeEvent(QResizeEvent *event) { + qreal left, top, right, bottom; + chart()->layout()->getContentsMargins(&left, &top, &right, &bottom); + move_icon->setPos(left, top); + close_btn_proxy->setPos(rect().right() - right - close_btn_proxy->size().width(), top); + int x = close_btn_proxy->pos().x() - manage_btn_proxy->size().width() - style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing); + manage_btn_proxy->setPos(x, top); + if (align_to > 0) { + updatePlotArea(align_to, true); + } + QChartView::resizeEvent(event); +} + +void ChartView::updatePlotArea(int left_pos, bool force) { + if (align_to != left_pos || force) { + align_to = left_pos; + + qreal left, top, right, bottom; + chart()->layout()->getContentsMargins(&left, &top, &right, &bottom); + chart()->legend()->setGeometry({move_icon->sceneBoundingRect().topRight(), manage_btn_proxy->sceneBoundingRect().bottomLeft()}); + QSizeF x_label_size = QFontMetrics(axis_x->labelsFont()).size(Qt::TextSingleLine, QString::number(axis_x->max(), 'f', 2)); + x_label_size += QSizeF{5, 5}; + int adjust_top = chart()->legend()->geometry().height() + style()->pixelMetric(QStyle::PM_LayoutTopMargin); + chart()->setPlotArea(rect().adjusted(align_to + left, adjust_top + top, -x_label_size.width() / 2 - right, -x_label_size.height() - bottom)); + chart()->layout()->invalidate(); + resetChartCache(); + } +} + +void ChartView::updateTitle() { + for (QLegendMarker *marker : chart()->legend()->markers()) { + QObject::connect(marker, &QLegendMarker::clicked, this, &ChartView::handleMarkerClicked, Qt::UniqueConnection); + } + for (auto &s : sigs) { + auto decoration = s.series->isVisible() ? "none" : "line-through"; + s.series->setName(QString("%2 %3 %4").arg(decoration, s.sig->name, msgName(s.msg_id), s.msg_id.toString())); + } + split_chart_act->setEnabled(sigs.size() > 1); + resetChartCache(); +} + +void ChartView::updatePlot(double cur, double min, double max) { + cur_sec = cur; + if (min != axis_x->min() || max != axis_x->max()) { + axis_x->setRange(min, max); + updateAxisY(); + updateSeriesPoints(); + // update tooltip + if (tooltip_x >= 0) { + showTip(chart()->mapToValue({tooltip_x, 0}).x()); + } + resetChartCache(); + } + viewport()->update(); +} + +void ChartView::updateSeriesPoints() { + // Show points when zoomed in enough + for (auto &s : sigs) { + auto begin = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->min(), xLessThan); + auto end = std::lower_bound(begin, s.vals.end(), axis_x->max(), xLessThan); + if (begin != end) { + int num_points = std::max((end - begin), 1); + QPointF right_pt = end == s.vals.end() ? s.vals.back() : *end; + double pixels_per_point = (chart()->mapToPosition(right_pt).x() - chart()->mapToPosition(*begin).x()) / num_points; + + if (series_type == SeriesType::Scatter) { + qreal size = std::clamp(pixels_per_point / 2.0, 2.0, 8.0); + if (s.series->useOpenGL()) { + size *= devicePixelRatioF(); + } + ((QScatterSeries *)s.series)->setMarkerSize(size); + } else { + s.series->setPointsVisible(pixels_per_point > 20); + } + } + } +} + +void ChartView::updateSeries(const cabana::Signal *sig) { + for (auto &s : sigs) { + if (!sig || s.sig == sig) { + if (!can->liveStreaming()) { + s.vals.clear(); + s.step_vals.clear(); + s.last_value_mono_time = 0; + } + s.series->setColor(getColor(s.sig)); + + const auto &msgs = can->events(s.msg_id); + auto first = std::upper_bound(msgs.cbegin(), msgs.cend(), s.last_value_mono_time, [](uint64_t ts, auto e) { + return ts < e->mono_time; + }); + int new_size = std::max(s.vals.size() + std::distance(first, msgs.cend()), settings.max_cached_minutes * 60 * 100); + if (s.vals.capacity() <= new_size) { + s.vals.reserve(new_size * 2); + s.step_vals.reserve(new_size * 4); + } + + const double route_start_time = can->routeStartTime(); + for (auto end = msgs.cend(); first != end; ++first) { + const CanEvent *e = *first; + double value = get_raw_value(e->dat, e->size, *s.sig); + double ts = e->mono_time / 1e9 - route_start_time; // seconds + s.vals.append({ts, value}); + if (!s.step_vals.empty()) { + s.step_vals.append({ts, s.step_vals.back().y()}); + } + s.step_vals.append({ts, value}); + s.last_value_mono_time = e->mono_time; + } + if (!can->liveStreaming()) { + s.segment_tree.build(s.vals); + } + s.series->replace(series_type == SeriesType::StepLine ? s.step_vals : s.vals); + } + } + updateAxisY(); + // invoke resetChartCache in ui thread + QMetaObject::invokeMethod(this, &ChartView::resetChartCache, Qt::QueuedConnection); +} + +// auto zoom on yaxis +void ChartView::updateAxisY() { + if (sigs.isEmpty()) return; + + double min = std::numeric_limits::max(); + double max = std::numeric_limits::lowest(); + QString unit = sigs[0].sig->unit; + + for (auto &s : sigs) { + if (!s.series->isVisible()) continue; + + // Only show unit when all signals have the same unit + if (unit != s.sig->unit) { + unit.clear(); + } + + auto first = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->min(), xLessThan); + auto last = std::lower_bound(first, s.vals.end(), axis_x->max(), xLessThan); + s.min = std::numeric_limits::max(); + s.max = std::numeric_limits::lowest(); + if (can->liveStreaming()) { + for (auto it = first; it != last; ++it) { + if (it->y() < s.min) s.min = it->y(); + if (it->y() > s.max) s.max = it->y(); + } + } else { + auto [min_y, max_y] = s.segment_tree.minmax(std::distance(s.vals.begin(), first), std::distance(s.vals.begin(), last)); + s.min = min_y; + s.max = max_y; + } + min = std::min(min, s.min); + max = std::max(max, s.max); + } + if (min == std::numeric_limits::max()) min = 0; + if (max == std::numeric_limits::lowest()) max = 0; + + if (axis_y->titleText() != unit) { + axis_y->setTitleText(unit); + y_label_width = 0; // recalc width + } + + double delta = std::abs(max - min) < 1e-3 ? 1 : (max - min) * 0.05; + auto [min_y, max_y, tick_count] = getNiceAxisNumbers(min - delta, max + delta, 3); + if (min_y != axis_y->min() || max_y != axis_y->max() || y_label_width == 0) { + axis_y->setRange(min_y, max_y); + axis_y->setTickCount(tick_count); + + int n = qMax(int(-qFloor(std::log10((max_y - min_y) / (tick_count - 1)))), 0) + 1; + int max_label_width = 0; + QFontMetrics fm(axis_y->labelsFont()); + for (int i = 0; i < tick_count; i++) { + qreal value = min_y + (i * (max_y - min_y) / (tick_count - 1)); + max_label_width = std::max(max_label_width, fm.width(QString::number(value, 'f', n))); + } + + int title_spacing = unit.isEmpty() ? 0 : QFontMetrics(axis_y->titleFont()).size(Qt::TextSingleLine, unit).height(); + y_label_width = title_spacing + max_label_width + 15; + axis_y->setLabelFormat(QString("%.%1f").arg(n)); + emit axisYLabelWidthChanged(y_label_width); + } +} + +std::tuple ChartView::getNiceAxisNumbers(qreal min, qreal max, int tick_count) { + qreal range = niceNumber((max - min), true); // range with ceiling + qreal step = niceNumber(range / (tick_count - 1), false); + min = qFloor(min / step); + max = qCeil(max / step); + tick_count = int(max - min) + 1; + return {min * step, max * step, tick_count}; +} + +// nice numbers can be expressed as form of 1*10^n, 2* 10^n or 5*10^n +qreal ChartView::niceNumber(qreal x, bool ceiling) { + qreal z = qPow(10, qFloor(std::log10(x))); //find corresponding number of the form of 10^n than is smaller than x + qreal q = x / z; //q<10 && q>=1; + if (ceiling) { + if (q <= 1.0) q = 1; + else if (q <= 2.0) q = 2; + else if (q <= 5.0) q = 5; + else q = 10; + } else { + if (q < 1.5) q = 1; + else if (q < 3.0) q = 2; + else if (q < 7.0) q = 5; + else q = 10; + } + return q * z; +} + +void ChartView::leaveEvent(QEvent *event) { + if (tip_label.isVisible()) { + charts_widget->showValueTip(-1); + } + QChartView::leaveEvent(event); +} + +QPixmap getBlankShadowPixmap(const QPixmap &px, int radius) { + QGraphicsDropShadowEffect *e = new QGraphicsDropShadowEffect; + e->setColor(QColor(40, 40, 40, 245)); + e->setOffset(0, 0); + e->setBlurRadius(radius); + + qreal dpr = px.devicePixelRatio(); + QPixmap blank(px.size()); + blank.setDevicePixelRatio(dpr); + blank.fill(Qt::white); + + QGraphicsScene scene; + QGraphicsPixmapItem item(blank); + item.setGraphicsEffect(e); + scene.addItem(&item); + + QPixmap shadow(px.size() + QSize(radius * dpr * 2, radius * dpr * 2)); + shadow.setDevicePixelRatio(dpr); + shadow.fill(Qt::transparent); + QPainter p(&shadow); + scene.render(&p, {QPoint(), shadow.size() / dpr}, item.boundingRect().adjusted(-radius, -radius, radius, radius)); + return shadow; +} + +static QPixmap getDropPixmap(const QPixmap &src) { + static QPixmap shadow_px; + const int radius = 10; + if (shadow_px.size() != src.size() + QSize(radius * 2, radius * 2)) { + shadow_px = getBlankShadowPixmap(src, radius); + } + QPixmap px = shadow_px; + QPainter p(&px); + QRectF target_rect(QPointF(radius, radius), src.size() / src.devicePixelRatio()); + p.drawPixmap(target_rect.topLeft(), src); + p.setCompositionMode(QPainter::CompositionMode_DestinationIn); + p.fillRect(target_rect, QColor(0, 0, 0, 200)); + return px; +} + +void ChartView::mousePressEvent(QMouseEvent *event) { + if (event->button() == Qt::LeftButton && move_icon->sceneBoundingRect().contains(event->pos())) { + QMimeData *mimeData = new QMimeData; + mimeData->setData(CHART_MIME_TYPE, QByteArray::number((qulonglong)this)); + QPixmap px = grab().scaledToWidth(CHART_MIN_WIDTH * viewport()->devicePixelRatio(), Qt::SmoothTransformation); + charts_widget->stopAutoScroll(); + QDrag *drag = new QDrag(this); + drag->setMimeData(mimeData); + drag->setPixmap(getDropPixmap(px)); + drag->setHotSpot(-QPoint(5, 5)); + drag->exec(Qt::CopyAction | Qt::MoveAction, Qt::MoveAction); + } else if (event->button() == Qt::LeftButton && QApplication::keyboardModifiers().testFlag(Qt::ShiftModifier)) { + // Save current playback state when scrubbing + resume_after_scrub = !can->isPaused(); + if (resume_after_scrub) { + can->pause(true); + } + is_scrubbing = true; + } else { + QChartView::mousePressEvent(event); + } +} + +void ChartView::mouseReleaseEvent(QMouseEvent *event) { + auto rubber = findChild(); + if (event->button() == Qt::LeftButton && rubber && rubber->isVisible()) { + rubber->hide(); + QRectF rect = rubber->geometry().normalized(); + double min = chart()->mapToValue(rect.topLeft()).x(); + double max = chart()->mapToValue(rect.bottomRight()).x(); + + // Prevent zooming/seeking past the end of the route + min = std::clamp(min, 0., can->totalSeconds()); + max = std::clamp(max, 0., can->totalSeconds()); + + if (rubber->width() <= 0) { + // no rubber dragged, seek to mouse position + can->seekTo(min); + } else if (rubber->width() > 10) { + charts_widget->zoom_undo_stack->push(new ZoomCommand(charts_widget, {min, max})); + } else { + viewport()->update(); + } + event->accept(); + } else if (event->button() == Qt::RightButton) { + charts_widget->zoom_undo_stack->undo(); + event->accept(); + } else { + QGraphicsView::mouseReleaseEvent(event); + } + + // Resume playback if we were scrubbing + is_scrubbing = false; + if (resume_after_scrub) { + can->pause(false); + resume_after_scrub = false; + } +} + +void ChartView::mouseMoveEvent(QMouseEvent *ev) { + const auto plot_area = chart()->plotArea(); + // Scrubbing + if (is_scrubbing && QApplication::keyboardModifiers().testFlag(Qt::ShiftModifier)) { + if (plot_area.contains(ev->pos())) { + can->seekTo(std::clamp(chart()->mapToValue(ev->pos()).x(), 0., can->totalSeconds())); + } + } + + auto rubber = findChild(); + bool is_zooming = rubber && rubber->isVisible(); + clearTrackPoints(); + + if (!is_zooming && plot_area.contains(ev->pos())) { + const double sec = chart()->mapToValue(ev->pos()).x(); + charts_widget->showValueTip(sec); + } else if (tip_label.isVisible()) { + charts_widget->showValueTip(-1); + } + + QChartView::mouseMoveEvent(ev); + if (is_zooming) { + QRect rubber_rect = rubber->geometry(); + rubber_rect.setLeft(std::max(rubber_rect.left(), (int)plot_area.left())); + rubber_rect.setRight(std::min(rubber_rect.right(), (int)plot_area.right())); + if (rubber_rect != rubber->geometry()) { + rubber->setGeometry(rubber_rect); + } + viewport()->update(); + } +} + +void ChartView::showTip(double sec) { + QRect tip_area(0, chart()->plotArea().top(), rect().width(), chart()->plotArea().height()); + QRect visible_rect = charts_widget->chartVisibleRect(this).intersected(tip_area); + if (visible_rect.isEmpty()) { + tip_label.hide(); + return; + } + + tooltip_x = chart()->mapToPosition({sec, 0}).x(); + qreal x = tooltip_x; + QStringList text_list(QString::number(chart()->mapToValue({x, 0}).x(), 'f', 3)); + for (auto &s : sigs) { + if (s.series->isVisible()) { + QString value = "--"; + // use reverse iterator to find last item <= sec. + auto it = std::lower_bound(s.vals.rbegin(), s.vals.rend(), sec, [](auto &p, double x) { return p.x() > x; }); + if (it != s.vals.rend() && it->x() >= axis_x->min()) { + value = QString::number(it->y()); + s.track_pt = *it; + x = std::max(x, chart()->mapToPosition(*it).x()); + } + QString name = sigs.size() > 1 ? s.sig->name + ": " : ""; + QString min = s.min == std::numeric_limits::max() ? "--" : QString::number(s.min); + QString max = s.max == std::numeric_limits::lowest() ? "--" : QString::number(s.max); + text_list << QString("%2%3 (%4, %5)") + .arg(s.series->color().name(), name, value, min, max); + } + } + QPoint pt(x, chart()->plotArea().top()); + QString text = "

" % text_list.join("
") % "

"; + tip_label.showText(pt, text, this, visible_rect); + viewport()->update(); +} + +void ChartView::hideTip() { + clearTrackPoints(); + tooltip_x = -1; + tip_label.hide(); + viewport()->update(); +} + +void ChartView::dragEnterEvent(QDragEnterEvent *event) { + if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { + drawDropIndicator(event->source() != this); + event->acceptProposedAction(); + } +} + +void ChartView::dragMoveEvent(QDragMoveEvent *event) { + if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { + event->setDropAction(event->source() == this ? Qt::MoveAction : Qt::CopyAction); + event->accept(); + } + charts_widget->startAutoScroll(); +} + +void ChartView::dropEvent(QDropEvent *event) { + if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { + if (event->source() != this) { + ChartView *source_chart = (ChartView *)event->source(); + for (auto &s : source_chart->sigs) { + source_chart->chart()->removeSeries(s.series); + addSeries(s.series); + } + sigs.append(source_chart->sigs); + updateAxisY(); + updateTitle(); + startAnimation(); + + source_chart->sigs.clear(); + charts_widget->removeChart(source_chart); + event->acceptProposedAction(); + } + can_drop = false; + } +} + +void ChartView::resetChartCache() { + chart_pixmap = QPixmap(); + viewport()->update(); +} + +void ChartView::startAnimation() { + QGraphicsOpacityEffect *eff = new QGraphicsOpacityEffect(this); + viewport()->setGraphicsEffect(eff); + QPropertyAnimation *a = new QPropertyAnimation(eff, "opacity"); + a->setDuration(250); + a->setStartValue(0.3); + a->setEndValue(1); + a->setEasingCurve(QEasingCurve::InBack); + a->start(QPropertyAnimation::DeleteWhenStopped); +} + +void ChartView::paintEvent(QPaintEvent *event) { + if (!can->liveStreaming()) { + if (chart_pixmap.isNull()) { + const qreal dpr = viewport()->devicePixelRatioF(); + chart_pixmap = QPixmap(viewport()->size() * dpr); + chart_pixmap.setDevicePixelRatio(dpr); + QPainter p(&chart_pixmap); + p.setRenderHints(QPainter::Antialiasing); + drawBackground(&p, viewport()->rect()); + scene()->setSceneRect(viewport()->rect()); + scene()->render(&p, viewport()->rect()); + } + + QPainter painter(viewport()); + painter.setRenderHints(QPainter::Antialiasing); + painter.drawPixmap(QPoint(), chart_pixmap); + if (can_drop) { + painter.setPen(QPen(palette().color(QPalette::Highlight), 4)); + painter.drawRect(viewport()->rect()); + } + QRectF exposed_rect = mapToScene(event->region().boundingRect()).boundingRect(); + drawForeground(&painter, exposed_rect); + } else { + QChartView::paintEvent(event); + } +} + +void ChartView::drawBackground(QPainter *painter, const QRectF &rect) { + painter->fillRect(rect, palette().color(QPalette::Base)); +} + +void ChartView::drawForeground(QPainter *painter, const QRectF &rect) { + // draw time line + qreal x = chart()->mapToPosition(QPointF{cur_sec, 0}).x(); + x = std::clamp(x, chart()->plotArea().left(), chart()->plotArea().right()); + qreal y1 = chart()->plotArea().top() - 2; + qreal y2 = chart()->plotArea().bottom() + 2; + painter->setPen(QPen(chart()->titleBrush().color(), 2)); + painter->drawLine(QPointF{x, y1}, QPointF{x, y2}); + + // draw track points + painter->setPen(Qt::NoPen); + qreal track_line_x = -1; + for (auto &s : sigs) { + if (!s.track_pt.isNull() && s.series->isVisible()) { + painter->setBrush(s.series->color().darker(125)); + QPointF pos = chart()->mapToPosition(s.track_pt); + painter->drawEllipse(pos, 5.5, 5.5); + track_line_x = std::max(track_line_x, pos.x()); + } + } + if (track_line_x > 0) { + painter->setPen(QPen(Qt::darkGray, 1, Qt::DashLine)); + painter->drawLine(QPointF{track_line_x, y1}, QPointF{track_line_x, y2}); + } + + // paint points. OpenGL mode lacks certain features (such as showing points) + painter->setPen(Qt::NoPen); + for (auto &s : sigs) { + if (s.series->useOpenGL() && s.series->isVisible() && s.series->pointsVisible()) { + auto first = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->min(), xLessThan); + auto last = std::lower_bound(first, s.vals.end(), axis_x->max(), xLessThan); + painter->setBrush(s.series->color()); + for (auto it = first; it != last; ++it) { + painter->drawEllipse(chart()->mapToPosition(*it), 4, 4); + } + } + } + + // paint zoom range + auto rubber = findChild(); + if (rubber && rubber->isVisible() && rubber->width() > 1) { + painter->setPen(Qt::white); + auto rubber_rect = rubber->geometry().normalized(); + for (const auto &pt : {rubber_rect.bottomLeft(), rubber_rect.bottomRight()}) { + QString sec = QString::number(chart()->mapToValue(pt).x(), 'f', 1); + // ChartAxisElement's padding is 4 (https://codebrowser.dev/qt5/qtcharts/src/charts/axis/chartaxiselement_p.h.html) + auto r = painter->fontMetrics().boundingRect(sec).adjusted(-6, -4, 6, 4); + pt == rubber_rect.bottomLeft() ? r.moveTopRight(pt + QPoint{0, 2}) : r.moveTopLeft(pt + QPoint{0, 2}); + painter->fillRect(r, Qt::gray); + painter->drawText(r, Qt::AlignCenter, sec); + } + } +} + +QXYSeries *ChartView::createSeries(SeriesType type, QColor color) { + QXYSeries *series = nullptr; + if (type == SeriesType::Line) { + series = new QLineSeries(this); + chart()->legend()->setMarkerShape(QLegend::MarkerShapeRectangle); + } else if (type == SeriesType::StepLine) { + series = new QLineSeries(this); + chart()->legend()->setMarkerShape(QLegend::MarkerShapeFromSeries); + } else { + series = new QScatterSeries(this); + chart()->legend()->setMarkerShape(QLegend::MarkerShapeCircle); + } + series->setColor(color); + // TODO: Due to a bug in CameraWidget the camera frames + // are drawn instead of the graphs on MacOS. Re-enable OpenGL when fixed +#ifndef __APPLE__ + series->setUseOpenGL(true); + // Qt doesn't properly apply device pixel ratio in OpenGL mode + QPen pen = series->pen(); + pen.setWidthF(2.0 * devicePixelRatioF()); + series->setPen(pen); +#endif + addSeries(series); + return series; +} + +void ChartView::addSeries(QXYSeries *series) { + chart()->addSeries(series); + series->attachAxis(axis_x); + series->attachAxis(axis_y); + + // disables the delivery of mouse events to the opengl widget. + // this enables the user to select the zoom area when the mouse press on the data point. + auto glwidget = findChild(); + if (glwidget && !glwidget->testAttribute(Qt::WA_TransparentForMouseEvents)) { + glwidget->setAttribute(Qt::WA_TransparentForMouseEvents); + } +} + +void ChartView::setSeriesType(SeriesType type) { + if (type != series_type) { + series_type = type; + for (auto &s : sigs) { + chart()->removeSeries(s.series); + s.series->deleteLater(); + } + for (auto &s : sigs) { + auto series = createSeries(series_type, getColor(s.sig)); + series->replace(series_type == SeriesType::StepLine ? s.step_vals : s.vals); + s.series = series; + } + updateSeriesPoints(); + updateTitle(); + } +} + +void ChartView::handleMarkerClicked() { + auto marker = qobject_cast(sender()); + Q_ASSERT(marker); + if (sigs.size() > 1) { + auto series = marker->series(); + series->setVisible(!series->isVisible()); + marker->setVisible(true); + updateAxisY(); + updateTitle(); + } +} diff --git a/tools/cabana/chart/chart.h b/tools/cabana/chart/chart.h new file mode 100644 index 0000000000..fda9271560 --- /dev/null +++ b/tools/cabana/chart/chart.h @@ -0,0 +1,109 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +using namespace QtCharts; + +#include "tools/cabana/chart/tiplabel.h" +#include "tools/cabana/dbc/dbcmanager.h" +#include "tools/cabana/streams/abstractstream.h" + +enum class SeriesType { + Line = 0, + StepLine, + Scatter +}; + +class ChartsWidget; +class ChartView : public QChartView { + Q_OBJECT + +public: + ChartView(const std::pair &x_range, ChartsWidget *parent = nullptr); + void addSignal(const MessageId &msg_id, const cabana::Signal *sig); + bool hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const; + void updateSeries(const cabana::Signal *sig = nullptr); + void updatePlot(double cur, double min, double max); + void setSeriesType(SeriesType type); + void updatePlotArea(int left, bool force = false); + void showTip(double sec); + void hideTip(); + void startAnimation(); + + struct SigItem { + MessageId msg_id; + const cabana::Signal *sig = nullptr; + QXYSeries *series = nullptr; + QVector vals; + QVector step_vals; + uint64_t last_value_mono_time = 0; + QPointF track_pt{}; + SegmentTree segment_tree; + double min = 0; + double max = 0; + }; + +signals: + void axisYLabelWidthChanged(int w); + +private slots: + void signalUpdated(const cabana::Signal *sig); + void manageSignals(); + void handleMarkerClicked(); + void msgUpdated(MessageId id); + void msgRemoved(MessageId id) { removeIf([=](auto &s) { return s.msg_id == id; }); } + void signalRemoved(const cabana::Signal *sig) { removeIf([=](auto &s) { return s.sig == sig; }); } + +private: + void createToolButtons(); + void addSeries(QXYSeries *series); + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *ev) override; + void dragEnterEvent(QDragEnterEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override { drawDropIndicator(false); } + void dragMoveEvent(QDragMoveEvent *event) override; + void dropEvent(QDropEvent *event) override; + void leaveEvent(QEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + QSize sizeHint() const override; + void updateAxisY(); + void updateTitle(); + void resetChartCache(); + void setTheme(QChart::ChartTheme theme); + void paintEvent(QPaintEvent *event) override; + void drawForeground(QPainter *painter, const QRectF &rect) override; + void drawBackground(QPainter *painter, const QRectF &rect) override; + void drawDropIndicator(bool draw) { if (std::exchange(can_drop, draw) != can_drop) viewport()->update(); } + std::tuple getNiceAxisNumbers(qreal min, qreal max, int tick_count); + qreal niceNumber(qreal x, bool ceiling); + QXYSeries *createSeries(SeriesType type, QColor color); + void updateSeriesPoints(); + void removeIf(std::function predicate); + inline void clearTrackPoints() { for (auto &s : sigs) s.track_pt = {}; } + + int y_label_width = 0; + int align_to = 0; + QValueAxis *axis_x; + QValueAxis *axis_y; + QAction *split_chart_act; + QGraphicsPixmapItem *move_icon; + QGraphicsProxyWidget *close_btn_proxy; + QGraphicsProxyWidget *manage_btn_proxy; + TipLabel tip_label; + QList sigs; + double cur_sec = 0; + SeriesType series_type = SeriesType::Line; + bool is_scrubbing = false; + bool resume_after_scrub = false; + QPixmap chart_pixmap; + bool can_drop = false; + double tooltip_x = -1; + ChartsWidget *charts_widget; + friend class ChartsWidget; +}; diff --git a/tools/cabana/chart/chartswidget.cc b/tools/cabana/chart/chartswidget.cc new file mode 100644 index 0000000000..ffb354aa3a --- /dev/null +++ b/tools/cabana/chart/chartswidget.cc @@ -0,0 +1,528 @@ +#include "tools/cabana/chart/chartswidget.h" + +#include +#include +#include +#include +#include +#include + +#include "tools/cabana/chart/chart.h" + +const int MAX_COLUMN_COUNT = 4; +const int CHART_SPACING = 10; + +ChartsWidget::ChartsWidget(QWidget *parent) : align_timer(this), auto_scroll_timer(this), QFrame(parent) { + setFrameStyle(QFrame::StyledPanel | QFrame::Plain); + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + main_layout->setSpacing(0); + + // toolbar + QToolBar *toolbar = new QToolBar(tr("Charts"), this); + int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize); + toolbar->setIconSize({icon_size, icon_size}); + + auto new_plot_btn = new ToolButton("file-plus", tr("New Chart")); + auto new_tab_btn = new ToolButton("window-stack", tr("New Tab")); + toolbar->addWidget(new_plot_btn); + toolbar->addWidget(new_tab_btn); + toolbar->addWidget(title_label = new QLabel()); + title_label->setContentsMargins(0, 0, style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing), 0); + + QMenu *menu = new QMenu(this); + for (int i = 0; i < MAX_COLUMN_COUNT; ++i) { + menu->addAction(tr("%1").arg(i + 1), [=]() { setColumnCount(i + 1); }); + } + columns_action = toolbar->addAction(""); + columns_action->setMenu(menu); + qobject_cast(toolbar->widgetForAction(columns_action))->setPopupMode(QToolButton::InstantPopup); + + QLabel *stretch_label = new QLabel(this); + stretch_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + toolbar->addWidget(stretch_label); + + range_lb_action = toolbar->addWidget(range_lb = new QLabel(this)); + range_slider = new LogSlider(1000, Qt::Horizontal, this); + range_slider->setMaximumWidth(200); + range_slider->setToolTip(tr("Set the chart range")); + range_slider->setRange(1, settings.max_cached_minutes * 60); + range_slider->setSingleStep(1); + range_slider->setPageStep(60); // 1 min + range_slider_action = toolbar->addWidget(range_slider); + + // zoom controls + zoom_undo_stack = new QUndoStack(this); + toolbar->addAction(undo_zoom_action = zoom_undo_stack->createUndoAction(this)); + undo_zoom_action->setIcon(utils::icon("arrow-counterclockwise")); + toolbar->addAction(redo_zoom_action = zoom_undo_stack->createRedoAction(this)); + redo_zoom_action->setIcon(utils::icon("arrow-clockwise")); + reset_zoom_action = toolbar->addWidget(reset_zoom_btn = new ToolButton("zoom-out", tr("Reset Zoom"))); + reset_zoom_btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + + toolbar->addWidget(remove_all_btn = new ToolButton("x", tr("Remove all charts"))); + toolbar->addWidget(dock_btn = new ToolButton("")); + main_layout->addWidget(toolbar); + + // tabbar + tabbar = new QTabBar(this); + tabbar->setAutoHide(true); + tabbar->setExpanding(false); + tabbar->setDrawBase(true); + tabbar->setAcceptDrops(true); + tabbar->setChangeCurrentOnDrag(true); + tabbar->setTabsClosable(true); + tabbar->setUsesScrollButtons(true); + main_layout->addWidget(tabbar); + + // charts + charts_container = new ChartsContainer(this); + + charts_scroll = new QScrollArea(this); + charts_scroll->setFrameStyle(QFrame::NoFrame); + charts_scroll->setWidgetResizable(true); + charts_scroll->setWidget(charts_container); + charts_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + main_layout->addWidget(charts_scroll); + + // init settings + current_theme = settings.theme; + column_count = std::clamp(settings.chart_column_count, 1, MAX_COLUMN_COUNT); + max_chart_range = std::clamp(settings.chart_range, 1, settings.max_cached_minutes * 60); + display_range = {0, max_chart_range}; + range_slider->setValue(max_chart_range); + updateToolBar(); + + align_timer.setSingleShot(true); + QObject::connect(&align_timer, &QTimer::timeout, this, &ChartsWidget::alignCharts); + 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(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); + QObject::connect(reset_zoom_btn, &QToolButton::clicked, this, &ChartsWidget::zoomReset); + QObject::connect(&settings, &Settings::changed, this, &ChartsWidget::settingChanged); + QObject::connect(new_tab_btn, &QToolButton::clicked, this, &ChartsWidget::newTab); + QObject::connect(this, &ChartsWidget::seriesChanged, this, &ChartsWidget::updateTabBar); + QObject::connect(tabbar, &QTabBar::tabCloseRequested, this, &ChartsWidget::removeTab); + QObject::connect(tabbar, &QTabBar::currentChanged, [this](int index) { + if (index != -1) updateLayout(true); + }); + QObject::connect(dock_btn, &QToolButton::clicked, [this]() { + emit dock(!docking); + docking = !docking; + updateToolBar(); + }); + + newTab(); + setWhatsThis(tr(R"( + Chart view
+ + )")); +} + +void ChartsWidget::newTab() { + static int tab_unique_id = 0; + int idx = tabbar->addTab(""); + tabbar->setTabData(idx, tab_unique_id++); + tabbar->setCurrentIndex(idx); + updateTabBar(); +} + +void ChartsWidget::removeTab(int index) { + int id = tabbar->tabData(index).toInt(); + for (auto &c : tab_charts[id]) { + removeChart(c); + } + tab_charts.erase(id); + tabbar->removeTab(index); + updateTabBar(); +} + +void ChartsWidget::updateTabBar() { + for (int i = 0; i < tabbar->count(); ++i) { + const auto &charts_in_tab = tab_charts[tabbar->tabData(i).toInt()]; + tabbar->setTabText(i, QString("Tab %1 (%2)").arg(i + 1).arg(charts_in_tab.count())); + } +} + +void ChartsWidget::eventsMerged() { + QFutureSynchronizer future_synchronizer; + for (auto c : charts) { + future_synchronizer.addFuture(QtConcurrent::run(c, &ChartView::updateSeries, nullptr)); + } +} + +void ChartsWidget::setZoom(double min, double max) { + zoomed_range = {min, max}; + is_zoomed = zoomed_range != display_range; + updateToolBar(); + updateState(); + emit rangeChanged(min, max, is_zoomed); +} + +void ChartsWidget::zoomReset() { + setZoom(display_range.first, display_range.second); + zoom_undo_stack->clear(); +} + +QRect ChartsWidget::chartVisibleRect(ChartView *chart) { + const QRect visible_rect(-charts_container->pos(), charts_scroll->viewport()->size()); + return chart->rect().intersected(QRect(chart->mapFrom(charts_container, visible_rect.topLeft()), visible_rect.size())); +} + +void ChartsWidget::showValueTip(double sec) { + for (auto c : currentCharts()) { + sec >= 0 ? c->showTip(sec) : c->hideTip(); + } +} + +void ChartsWidget::updateState() { + if (charts.isEmpty()) return; + + const double cur_sec = can->currentSec(); + if (!is_zoomed) { + double pos = (cur_sec - display_range.first) / std::max(1.0, max_chart_range); + if (pos < 0 || pos > 0.8) { + display_range.first = std::max(0.0, cur_sec - max_chart_range * 0.1); + } + double max_sec = std::min(std::floor(display_range.first + max_chart_range), can->lastEventSecond()); + display_range.first = std::max(0.0, max_sec - max_chart_range); + display_range.second = display_range.first + max_chart_range; + } else if (cur_sec < (zoomed_range.first - 0.1) || cur_sec >= zoomed_range.second) { + // loop in zoomed range + can->seekTo(zoomed_range.first); + } + + const auto &range = is_zoomed ? zoomed_range : display_range; + for (auto c : charts) { + c->updatePlot(cur_sec, range.first, range.second); + } +} + +void ChartsWidget::setMaxChartRange(int value) { + max_chart_range = settings.chart_range = range_slider->value(); + updateToolBar(); + updateState(); +} + +void ChartsWidget::updateToolBar() { + title_label->setText(tr("Charts: %1").arg(charts.size())); + columns_action->setText(tr("Column: %1").arg(column_count)); + range_lb->setText(utils::formatSeconds(max_chart_range)); + range_lb_action->setVisible(!is_zoomed); + range_slider_action->setVisible(!is_zoomed); + undo_zoom_action->setVisible(is_zoomed); + redo_zoom_action->setVisible(is_zoomed); + reset_zoom_action->setVisible(is_zoomed); + reset_zoom_btn->setText(is_zoomed ? tr("%1-%2").arg(zoomed_range.first, 0, 'f', 1).arg(zoomed_range.second, 0, 'f', 1) : ""); + remove_all_btn->setEnabled(!charts.isEmpty()); + dock_btn->setIcon(docking ? "arrow-up-right-square" : "arrow-down-left-square"); + dock_btn->setToolTip(docking ? tr("Undock charts") : tr("Dock charts")); +} + +void ChartsWidget::settingChanged() { + if (std::exchange(current_theme, settings.theme) != current_theme) { + undo_zoom_action->setIcon(utils::icon("arrow-counterclockwise")); + redo_zoom_action->setIcon(utils::icon("arrow-clockwise")); + auto theme = settings.theme == DARK_THEME ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight; + for (auto c : charts) { + c->setTheme(theme); + } + } + range_slider->setRange(1, settings.max_cached_minutes * 60); + for (auto c : charts) { + c->setFixedHeight(settings.chart_height); + c->setSeriesType((SeriesType)settings.chart_series_type); + c->resetChartCache(); + } +} + +ChartView *ChartsWidget::findChart(const MessageId &id, const cabana::Signal *sig) { + for (auto c : charts) + if (c->hasSignal(id, sig)) return c; + return nullptr; +} + +ChartView *ChartsWidget::createChart() { + auto chart = new ChartView(is_zoomed ? zoomed_range : display_range, this); + chart->setFixedHeight(settings.chart_height); + chart->setMinimumWidth(CHART_MIN_WIDTH); + chart->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); + QObject::connect(chart, &ChartView::axisYLabelWidthChanged, &align_timer, qOverload<>(&QTimer::start)); + charts.push_front(chart); + currentCharts().push_front(chart); + updateLayout(true); + updateToolBar(); + return chart; +} + +void ChartsWidget::showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge) { + ChartView *chart = findChart(id, sig); + if (show && !chart) { + chart = merge && currentCharts().size() > 0 ? currentCharts().front() : createChart(); + chart->addSignal(id, sig); + } else if (!show && chart) { + chart->removeIf([&](auto &s) { return s.msg_id == id && s.sig == sig; }); + } +} + +void ChartsWidget::splitChart(ChartView *src_chart) { + if (src_chart->sigs.size() > 1) { + for (auto it = src_chart->sigs.begin() + 1; it != src_chart->sigs.end(); /**/) { + auto c = createChart(); + src_chart->chart()->removeSeries(it->series); + c->addSeries(it->series); + c->sigs.push_back(*it); + c->updateAxisY(); + c->updateTitle(); + it = src_chart->sigs.erase(it); + } + src_chart->updateAxisY(); + src_chart->updateTitle(); + } +} + +void ChartsWidget::setColumnCount(int n) { + n = std::clamp(n, 1, MAX_COLUMN_COUNT); + if (column_count != n) { + column_count = settings.chart_column_count = n; + updateToolBar(); + updateLayout(); + } +} + +void ChartsWidget::updateLayout(bool force) { + auto charts_layout = charts_container->charts_layout; + int n = MAX_COLUMN_COUNT; + for (; n > 1; --n) { + if ((n * CHART_MIN_WIDTH + (n - 1) * charts_layout->horizontalSpacing()) < charts_layout->geometry().width()) break; + } + + bool show_column_cb = n > 1; + columns_action->setVisible(show_column_cb); + + n = std::min(column_count, n); + auto ¤t_charts = currentCharts(); + if ((current_charts.size() != charts_layout->count() || n != current_column_count) || force) { + current_column_count = n; + charts_container->setUpdatesEnabled(false); + for (auto c : charts) { + c->setVisible(false); + } + for (int i = 0; i < current_charts.size(); ++i) { + charts_layout->addWidget(current_charts[i], i / n, i % n); + if (current_charts[i]->sigs.isEmpty()) { + // the chart will be resized after add signal. delay setVisible to reduce flicker. + QTimer::singleShot(0, [c = current_charts[i]]() { c->setVisible(true); }); + } else { + current_charts[i]->setVisible(true); + } + } + charts_container->setUpdatesEnabled(true); + } +} + +void ChartsWidget::startAutoScroll() { + auto_scroll_timer.start(50); +} + +void ChartsWidget::stopAutoScroll() { + auto_scroll_timer.stop(); + auto_scroll_count = 0; +} + +void ChartsWidget::doAutoScroll() { + QScrollBar *scroll = charts_scroll->verticalScrollBar(); + if (auto_scroll_count < scroll->pageStep()) { + ++auto_scroll_count; + } + + int value = scroll->value(); + QPoint pos = charts_scroll->viewport()->mapFromGlobal(QCursor::pos()); + QRect area = charts_scroll->viewport()->rect(); + + if (pos.y() - area.top() < settings.chart_height / 2) { + scroll->setValue(value - auto_scroll_count); + } else if (area.bottom() - pos.y() < settings.chart_height / 2) { + scroll->setValue(value + auto_scroll_count); + } + bool vertical_unchanged = value == scroll->value(); + if (vertical_unchanged) { + stopAutoScroll(); + } else { + // mouseMoveEvent to updates the drag-selection rectangle + const QPoint globalPos = charts_scroll->viewport()->mapToGlobal(pos); + const QPoint windowPos = charts_scroll->window()->mapFromGlobal(globalPos); + QMouseEvent mm(QEvent::MouseMove, pos, windowPos, globalPos, + Qt::NoButton, Qt::LeftButton, Qt::NoModifier, Qt::MouseEventSynthesizedByQt); + QApplication::sendEvent(charts_scroll->viewport(), &mm); + } +} + +void ChartsWidget::resizeEvent(QResizeEvent *event) { + QWidget::resizeEvent(event); + updateLayout(); +} + +void ChartsWidget::newChart() { + SignalSelector dlg(tr("New Chart"), this); + if (dlg.exec() == QDialog::Accepted) { + auto items = dlg.seletedItems(); + if (!items.isEmpty()) { + auto c = createChart(); + for (auto it : items) { + c->addSignal(it->msg_id, it->sig); + } + } + } +} + +void ChartsWidget::removeChart(ChartView *chart) { + charts.removeOne(chart); + chart->deleteLater(); + for (auto &[_, list] : tab_charts) { + list.removeOne(chart); + } + updateToolBar(); + updateLayout(true); + alignCharts(); + emit seriesChanged(); +} + +void ChartsWidget::removeAll() { + while (tabbar->count() > 1) { + tabbar->removeTab(1); + } + tab_charts.clear(); + + if (!charts.isEmpty()) { + for (auto c : charts) { + c->deleteLater(); + } + charts.clear(); + updateToolBar(); + emit seriesChanged(); + } +} + +void ChartsWidget::alignCharts() { + int plot_left = 0; + for (auto c : charts) { + plot_left = std::max(plot_left, c->y_label_width); + } + plot_left = std::max((plot_left / 10) * 10 + 10, 50); + for (auto c : charts) { + c->updatePlotArea(plot_left); + } +} + +bool ChartsWidget::eventFilter(QObject *obj, QEvent *event) { + if (obj != this && event->type() == QEvent::Close) { + emit dock_btn->clicked(); + return true; + } + return false; +} + +bool ChartsWidget::event(QEvent *event) { + bool back_button = false; + switch (event->type()) { + case QEvent::MouseButtonPress: { + QMouseEvent *ev = static_cast(event); + back_button = ev->button() == Qt::BackButton; + break; + } + case QEvent::NativeGesture: { + QNativeGestureEvent *ev = static_cast(event); + back_button = (ev->value() == 180); + break; + } + case QEvent::WindowActivate: + case QEvent::WindowDeactivate: + case QEvent::FocusIn: + case QEvent::FocusOut: + case QEvent::Leave: + showValueTip(-1); + break; + default: + break; + } + + if (back_button) { + zoom_undo_stack->undo(); + return true; + } + return QFrame::event(event); +} + +// ChartsContainer + +ChartsContainer::ChartsContainer(ChartsWidget *parent) : charts_widget(parent), QWidget(parent) { + setAcceptDrops(true); + QVBoxLayout *charts_main_layout = new QVBoxLayout(this); + charts_main_layout->setContentsMargins(0, 10, 0, 0); + charts_layout = new QGridLayout(); + charts_layout->setSpacing(CHART_SPACING); + charts_main_layout->addLayout(charts_layout); + charts_main_layout->addStretch(0); +} + +void ChartsContainer::dragEnterEvent(QDragEnterEvent *event) { + if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { + event->acceptProposedAction(); + drawDropIndicator(event->pos()); + } +} + +void ChartsContainer::dropEvent(QDropEvent *event) { + if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { + auto w = getDropAfter(event->pos()); + auto chart = qobject_cast(event->source()); + if (w != chart) { + for (auto &[_, list] : charts_widget->tab_charts) { + list.removeOne(chart); + } + int to = w ? charts_widget->currentCharts().indexOf(w) + 1 : 0; + charts_widget->currentCharts().insert(to, chart); + charts_widget->updateLayout(true); + charts_widget->updateTabBar(); + event->acceptProposedAction(); + chart->startAnimation(); + } + drawDropIndicator({}); + } +} + +void ChartsContainer::paintEvent(QPaintEvent *ev) { + if (!drop_indictor_pos.isNull() && !childAt(drop_indictor_pos)) { + QRect r; + if (auto insert_after = getDropAfter(drop_indictor_pos)) { + QRect area = insert_after->geometry(); + r = QRect(area.left(), area.bottom() + 1, area.width(), CHART_SPACING); + } else { + r = geometry(); + r.setHeight(CHART_SPACING); + } + + const int margin = (CHART_SPACING - 2) / 2; + QPainterPath path; + path.addPolygon(QPolygonF({r.topLeft(), QPointF(r.left() + CHART_SPACING, r.top() + r.height() / 2), r.bottomLeft()})); + path.addPolygon(QPolygonF({r.topRight(), QPointF(r.right() - CHART_SPACING, r.top() + r.height() / 2), r.bottomRight()})); + + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + p.fillPath(path, palette().highlight()); + p.fillRect(r.adjusted(2, margin, -2, -margin), palette().highlight()); + } +} + +ChartView *ChartsContainer::getDropAfter(const QPoint &pos) const { + auto it = std::find_if(charts_widget->currentCharts().crbegin(), charts_widget->currentCharts().crend(), [&pos](auto c) { + auto area = c->geometry(); + return pos.x() >= area.left() && pos.x() <= area.right() && pos.y() >= area.bottom(); + }); + return it == charts_widget->currentCharts().crend() ? nullptr : *it; +} diff --git a/tools/cabana/chart/chartswidget.h b/tools/cabana/chart/chartswidget.h new file mode 100644 index 0000000000..25e5b8e194 --- /dev/null +++ b/tools/cabana/chart/chartswidget.h @@ -0,0 +1,128 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "tools/cabana/chart/signalselector.h" +#include "tools/cabana/dbc/dbcmanager.h" +#include "tools/cabana/streams/abstractstream.h" + +const int CHART_MIN_WIDTH = 300; +const QString CHART_MIME_TYPE = "application/x-cabanachartview"; + +class ChartView; +class ChartsWidget; + +class ChartsContainer : public QWidget { +public: + ChartsContainer(ChartsWidget *parent); + void dragEnterEvent(QDragEnterEvent *event) override; + void dropEvent(QDropEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override { drawDropIndicator({}); } + void drawDropIndicator(const QPoint &pt) { drop_indictor_pos = pt; update(); } + void paintEvent(QPaintEvent *ev) override; + ChartView *getDropAfter(const QPoint &pos) const; + + QGridLayout *charts_layout; + ChartsWidget *charts_widget; + QPoint drop_indictor_pos; +}; + +class ChartsWidget : public QFrame { + Q_OBJECT + +public: + ChartsWidget(QWidget *parent = nullptr); + void showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge); + inline bool hasSignal(const MessageId &id, const cabana::Signal *sig) { return findChart(id, sig) != nullptr; } + +public slots: + void setColumnCount(int n); + void removeAll(); + void setZoom(double min, double max); + +signals: + void dock(bool floating); + void rangeChanged(double min, double max, bool is_zommed); + void seriesChanged(); + +private: + void resizeEvent(QResizeEvent *event) override; + bool event(QEvent *event) override; + void alignCharts(); + void newChart(); + ChartView *createChart(); + void removeChart(ChartView *chart); + void splitChart(ChartView *chart); + QRect chartVisibleRect(ChartView *chart); + void eventsMerged(); + void updateState(); + void zoomReset(); + void startAutoScroll(); + void stopAutoScroll(); + void doAutoScroll(); + void updateToolBar(); + void updateTabBar(); + void setMaxChartRange(int value); + void updateLayout(bool force = false); + void settingChanged(); + void showValueTip(double sec); + bool eventFilter(QObject *obj, QEvent *event) override; + void newTab(); + void removeTab(int index); + inline QList ¤tCharts() { return tab_charts[tabbar->tabData(tabbar->currentIndex()).toInt()]; } + ChartView *findChart(const MessageId &id, const cabana::Signal *sig); + + QLabel *title_label; + QLabel *range_lb; + LogSlider *range_slider; + QAction *range_lb_action; + QAction *range_slider_action; + bool docking = true; + ToolButton *dock_btn; + + QAction *undo_zoom_action; + QAction *redo_zoom_action; + QAction *reset_zoom_action; + ToolButton *reset_zoom_btn; + QUndoStack *zoom_undo_stack; + + ToolButton *remove_all_btn; + QList charts; + std::unordered_map> tab_charts; + QTabBar *tabbar; + ChartsContainer *charts_container; + QScrollArea *charts_scroll; + uint32_t max_chart_range = 0; + bool is_zoomed = false; + std::pair display_range; + std::pair zoomed_range; + QAction *columns_action; + int column_count = 1; + int current_column_count = 0; + int auto_scroll_count = 0; + QTimer auto_scroll_timer; + QTimer align_timer; + int current_theme = 0; + friend class ZoomCommand; + friend class ChartView; + friend class ChartsContainer; +}; + +class ZoomCommand : public QUndoCommand { +public: + ZoomCommand(ChartsWidget *charts, std::pair range) : charts(charts), range(range), QUndoCommand() { + prev_range = charts->is_zoomed ? charts->zoomed_range : charts->display_range; + setText(QObject::tr("Zoom to %1-%2").arg(range.first, 0, 'f', 1).arg(range.second, 0, 'f', 1)); + } + void undo() override { charts->setZoom(prev_range.first, prev_range.second); } + void redo() override { charts->setZoom(range.first, range.second); } + ChartsWidget *charts; + std::pair prev_range, range; +}; + diff --git a/tools/cabana/chart/signalselector.cc b/tools/cabana/chart/signalselector.cc new file mode 100644 index 0000000000..1aa8fc5016 --- /dev/null +++ b/tools/cabana/chart/signalselector.cc @@ -0,0 +1,108 @@ +#include "tools/cabana/chart/signalselector.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "tools/cabana/streams/abstractstream.h" + +SignalSelector::SignalSelector(QString title, QWidget *parent) : QDialog(parent) { + setWindowTitle(title); + QGridLayout *main_layout = new QGridLayout(this); + + // left column + main_layout->addWidget(new QLabel(tr("Available Signals")), 0, 0); + main_layout->addWidget(msgs_combo = new QComboBox(this), 1, 0); + msgs_combo->setEditable(true); + msgs_combo->lineEdit()->setPlaceholderText(tr("Select a msg...")); + msgs_combo->setInsertPolicy(QComboBox::NoInsert); + msgs_combo->completer()->setCompletionMode(QCompleter::PopupCompletion); + msgs_combo->completer()->setFilterMode(Qt::MatchContains); + + main_layout->addWidget(available_list = new QListWidget(this), 2, 0); + + // buttons + QVBoxLayout *btn_layout = new QVBoxLayout(); + QPushButton *add_btn = new QPushButton(utils::icon("chevron-right"), "", this); + add_btn->setEnabled(false); + QPushButton *remove_btn = new QPushButton(utils::icon("chevron-left"), "", this); + remove_btn->setEnabled(false); + btn_layout->addStretch(0); + btn_layout->addWidget(add_btn); + btn_layout->addWidget(remove_btn); + btn_layout->addStretch(0); + main_layout->addLayout(btn_layout, 0, 1, 3, 1); + + // right column + main_layout->addWidget(new QLabel(tr("Selected Signals")), 0, 2); + main_layout->addWidget(selected_list = new QListWidget(this), 1, 2, 2, 1); + + 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())); + } + } + msgs_combo->model()->sort(0); + msgs_combo->setCurrentIndex(-1); + + QObject::connect(msgs_combo, qOverload(&QComboBox::currentIndexChanged), this, &SignalSelector::updateAvailableList); + QObject::connect(available_list, &QListWidget::currentRowChanged, [=](int row) { add_btn->setEnabled(row != -1); }); + QObject::connect(selected_list, &QListWidget::currentRowChanged, [=](int row) { remove_btn->setEnabled(row != -1); }); + QObject::connect(available_list, &QListWidget::itemDoubleClicked, this, &SignalSelector::add); + QObject::connect(selected_list, &QListWidget::itemDoubleClicked, this, &SignalSelector::remove); + QObject::connect(add_btn, &QPushButton::clicked, [this]() { if (auto item = available_list->currentItem()) add(item); }); + QObject::connect(remove_btn, &QPushButton::clicked, [this]() { if (auto item = selected_list->currentItem()) remove(item); }); + QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +void SignalSelector::add(QListWidgetItem *item) { + auto it = (ListItem *)item; + addItemToList(selected_list, it->msg_id, it->sig, true); + delete item; +} + +void SignalSelector::remove(QListWidgetItem *item) { + auto it = (ListItem *)item; + if (it->msg_id == msgs_combo->currentData().value()) { + addItemToList(available_list, it->msg_id, it->sig); + } + delete item; +} + +void SignalSelector::updateAvailableList(int index) { + if (index == -1) return; + available_list->clear(); + MessageId msg_id = msgs_combo->itemData(index).value(); + auto selected_items = seletedItems(); + for (auto s : dbc()->msg(msg_id)->getSignals()) { + bool is_selected = std::any_of(selected_items.begin(), selected_items.end(), [=, sig = s](auto it) { return it->msg_id == msg_id && it->sig == sig; }); + if (!is_selected) { + addItemToList(available_list, msg_id, s); + } + } +} + +void SignalSelector::addItemToList(QListWidget *parent, const MessageId id, const cabana::Signal *sig, bool show_msg_name) { + QString text = QString(" %1").arg(getColor(sig).name(), sig->name); + if (show_msg_name) text += QString(" %0 %1").arg(msgName(id), id.toString()); + + QLabel *label = new QLabel(text); + label->setContentsMargins(5, 0, 5, 0); + auto new_item = new ListItem(id, sig, parent); + new_item->setSizeHint(label->sizeHint()); + parent->setItemWidget(new_item, label); +} + +QList SignalSelector::seletedItems() { + QList ret; + for (int i = 0; i < selected_list->count(); ++i) ret.push_back((ListItem *)selected_list->item(i)); + return ret; +} diff --git a/tools/cabana/chart/signalselector.h b/tools/cabana/chart/signalselector.h new file mode 100644 index 0000000000..f46779f044 --- /dev/null +++ b/tools/cabana/chart/signalselector.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +#include "tools/cabana/dbc/dbcmanager.h" + +class SignalSelector : public QDialog { +public: + struct ListItem : public QListWidgetItem { + ListItem(const MessageId &msg_id, const cabana::Signal *sig, QListWidget *parent) : msg_id(msg_id), sig(sig), QListWidgetItem(parent) {} + MessageId msg_id; + const cabana::Signal *sig; + }; + + SignalSelector(QString title, QWidget *parent); + QList seletedItems(); + inline void addSelected(const MessageId &id, const cabana::Signal *sig) { addItemToList(selected_list, id, sig, true); } + +private: + void updateAvailableList(int index); + void addItemToList(QListWidget *parent, const MessageId id, const cabana::Signal *sig, bool show_msg_name = false); + void add(QListWidgetItem *item); + void remove(QListWidgetItem *item); + + QComboBox *msgs_combo; + QListWidget *available_list; + QListWidget *selected_list; +}; diff --git a/tools/cabana/chart/sparkline.cc b/tools/cabana/chart/sparkline.cc new file mode 100644 index 0000000000..6d7b35f3a9 --- /dev/null +++ b/tools/cabana/chart/sparkline.cc @@ -0,0 +1,74 @@ +#include "tools/cabana/chart/sparkline.h" + +#include + +#include "tools/cabana/streams/abstractstream.h" + +void Sparkline::update(const MessageId &msg_id, const cabana::Signal *sig, double last_msg_ts, int range, QSize size) { + const auto &msgs = can->events(msg_id); + uint64_t ts = (last_msg_ts + can->routeStartTime()) * 1e9; + uint64_t first_ts = (ts > range * 1e9) ? ts - range * 1e9 : 0; + auto first = std::lower_bound(msgs.cbegin(), msgs.cend(), first_ts, [](auto e, uint64_t ts) { + return e->mono_time < ts; + }); + auto last = std::upper_bound(first, msgs.cend(), ts, [](uint64_t ts, auto e) { + return ts < e->mono_time; + }); + + 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(); + values.reserve(std::distance(first, last)); + min_val = std::numeric_limits::max(); + max_val = std::numeric_limits::lowest(); + for (auto it = first; it != last; ++it) { + const CanEvent *e = *it; + double value = get_raw_value(e->dat, e->size, *sig); + 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; + } + } + render(getColor(sig), 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; + const double yscale = (size.height() - 3) / (max_val - min_val); + points.clear(); + points.reserve(values.size()); + for (auto &v : values) { + points.emplace_back(v.x() * xscale, 1 + std::abs(v.y() - max_val) * yscale); + } + + qreal dpr = qApp->devicePixelRatio(); + size *= dpr; + if (size != pixmap.size()) { + pixmap = QPixmap(size); + } + pixmap.setDevicePixelRatio(dpr); + pixmap.fill(Qt::transparent); + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing, points.size() < 500); + painter.setPen(color); + painter.drawPolyline(points.data(), points.size()); + painter.setPen(QPen(color, 3)); + if ((points.back().x() - points.front().x()) / points.size() > 8) { + painter.drawPoints(points.data(), points.size()); + } else { + painter.drawPoint(points.back()); + } +} diff --git a/tools/cabana/chart/sparkline.h b/tools/cabana/chart/sparkline.h new file mode 100644 index 0000000000..69d4f4bc55 --- /dev/null +++ b/tools/cabana/chart/sparkline.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +#include "tools/cabana/dbc/dbcmanager.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(); } + + 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 values; + std::vector points; +}; diff --git a/tools/cabana/chart/tiplabel.cc b/tools/cabana/chart/tiplabel.cc new file mode 100644 index 0000000000..3ee5f00978 --- /dev/null +++ b/tools/cabana/chart/tiplabel.cc @@ -0,0 +1,50 @@ +#include "tools/cabana/chart/tiplabel.h" + +#include +#include +#include + +#include "tools/cabana/settings.h" + +TipLabel::TipLabel(QWidget *parent) : QLabel(parent, Qt::ToolTip | Qt::FramelessWindowHint) { + setForegroundRole(QPalette::ToolTipText); + setBackgroundRole(QPalette::ToolTipBase); + auto palette = QToolTip::palette(); + if (settings.theme != DARK_THEME) { + palette.setColor(QPalette::ToolTipBase, QApplication::palette().color(QPalette::Base)); + palette.setColor(QPalette::ToolTipText, QRgb(0x404044)); // same color as chart label brush + } + setPalette(palette); + ensurePolished(); + setMargin(1 + style()->pixelMetric(QStyle::PM_ToolTipLabelFrameWidth, nullptr, this)); + setAttribute(Qt::WA_ShowWithoutActivating); + setTextFormat(Qt::RichText); + setVisible(false); +} + +void TipLabel::showText(const QPoint &pt, const QString &text, QWidget *w, const QRect &rect) { + setText(text); + if (!text.isEmpty()) { + QSize extra(1, 1); + resize(sizeHint() + extra); + QPoint tip_pos(pt.x() + 12, rect.top() + 2); + if (tip_pos.x() + size().width() >= rect.right()) { + tip_pos.rx() = pt.x() - size().width() - 12; + } + if (rect.contains({tip_pos, size()})) { + move(w->mapToGlobal(tip_pos)); + setVisible(true); + return; + } + } + setVisible(false); +} + +void TipLabel::paintEvent(QPaintEvent *ev) { + QStylePainter p(this); + QStyleOptionFrame opt; + opt.init(this); + p.drawPrimitive(QStyle::PE_PanelTipLabel, opt); + p.end(); + QLabel::paintEvent(ev); +} diff --git a/tools/cabana/chart/tiplabel.h b/tools/cabana/chart/tiplabel.h new file mode 100644 index 0000000000..ac6e09e976 --- /dev/null +++ b/tools/cabana/chart/tiplabel.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +class TipLabel : public QLabel { +public: + TipLabel(QWidget *parent = nullptr); + void showText(const QPoint &pt, const QString &sec, QWidget *w, const QRect &rect); + void paintEvent(QPaintEvent *ev) override; +}; diff --git a/tools/cabana/chartswidget.cc b/tools/cabana/chartswidget.cc deleted file mode 100644 index 09fcbf2ee2..0000000000 --- a/tools/cabana/chartswidget.cc +++ /dev/null @@ -1,981 +0,0 @@ -#include "tools/cabana/chartswidget.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -const int MAX_COLUMN_COUNT = 4; -static inline bool xLessThan(const QPointF &p, float x) { return p.x() < x; } - -// ChartsWidget - -ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { - setFrameStyle(QFrame::StyledPanel | QFrame::Plain); - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - - // toolbar - QToolBar *toolbar = new QToolBar(tr("Charts"), this); - int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize); - toolbar->setIconSize({icon_size, icon_size}); - - QAction *new_plot_btn = toolbar->addAction(utils::icon("file-plus"), tr("New Plot")); - toolbar->addWidget(title_label = new QLabel()); - title_label->setContentsMargins(0, 0, style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing), 0); - - QMenu *menu = new QMenu(this); - for (int i = 0; i < MAX_COLUMN_COUNT; ++i) { - menu->addAction(tr("%1").arg(i + 1), [=]() { setColumnCount(i + 1); }); - } - columns_action = toolbar->addAction(""); - columns_action->setMenu(menu); - qobject_cast(toolbar->widgetForAction(columns_action))->setPopupMode(QToolButton::InstantPopup); - - QLabel *stretch_label = new QLabel(this); - stretch_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - toolbar->addWidget(stretch_label); - - range_lb_action = toolbar->addWidget(range_lb = new QLabel(this)); - range_slider = new LogSlider(1000, Qt::Horizontal, this); - range_slider->setMaximumWidth(200); - range_slider->setToolTip(tr("Set the chart range")); - range_slider->setRange(1, settings.max_cached_minutes * 60); - range_slider->setSingleStep(1); - range_slider->setPageStep(60); // 1 min - range_slider_action = toolbar->addWidget(range_slider); - - reset_zoom_action = toolbar->addAction(utils::icon("zoom-out"), tr("Reset Zoom")); - qobject_cast(toolbar->widgetForAction(reset_zoom_action))->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - - remove_all_btn = toolbar->addAction(utils::icon("x"), tr("Remove all charts")); - dock_btn = toolbar->addAction(""); - main_layout->addWidget(toolbar); - - // charts - charts_layout = new QGridLayout(); - charts_layout->setSpacing(10); - - QWidget *charts_container = new QWidget(this); - QVBoxLayout *charts_main_layout = new QVBoxLayout(charts_container); - charts_main_layout->setContentsMargins(0, 0, 0, 0); - charts_main_layout->addLayout(charts_layout); - charts_main_layout->addStretch(0); - - QScrollArea *charts_scroll = new QScrollArea(this); - charts_scroll->setFrameStyle(QFrame::NoFrame); - charts_scroll->setWidgetResizable(true); - charts_scroll->setWidget(charts_container); - charts_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - main_layout->addWidget(charts_scroll); - - // init settings - use_dark_theme = QApplication::palette().color(QPalette::WindowText).value() > - QApplication::palette().color(QPalette::Background).value(); - column_count = std::clamp(settings.chart_column_count, 1, MAX_COLUMN_COUNT); - max_chart_range = std::clamp(settings.chart_range, 1, settings.max_cached_minutes * 60); - display_range = {0, max_chart_range}; - range_slider->setValue(max_chart_range); - updateToolBar(); - - 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(range_slider, &QSlider::valueChanged, this, &ChartsWidget::setMaxChartRange); - QObject::connect(new_plot_btn, &QAction::triggered, this, &ChartsWidget::newChart); - QObject::connect(remove_all_btn, &QAction::triggered, this, &ChartsWidget::removeAll); - QObject::connect(reset_zoom_action, &QAction::triggered, this, &ChartsWidget::zoomReset); - QObject::connect(&settings, &Settings::changed, this, &ChartsWidget::settingChanged); - QObject::connect(dock_btn, &QAction::triggered, [this]() { - emit dock(!docking); - docking = !docking; - updateToolBar(); - }); - - setWhatsThis(tr(R"( - Chart view
- - )")); -} - -void ChartsWidget::eventsMerged() { - { - QFutureSynchronizer future_synchronizer; - for (auto c : charts) { - future_synchronizer.addFuture(QtConcurrent::run(c, &ChartView::updateSeries, nullptr)); - } - } - if (can->isPaused()) { - updateState(); - } -} - -void ChartsWidget::zoomIn(double min, double max) { - zoomed_range = {min, max}; - is_zoomed = zoomed_range != display_range; - updateToolBar(); - updateState(); - emit rangeChanged(min, max, is_zoomed); -} - -void ChartsWidget::zoomReset() { - zoomIn(display_range.first, display_range.second); -} - -void ChartsWidget::updateState() { - if (charts.isEmpty()) return; - - const double cur_sec = can->currentSec(); - if (!is_zoomed) { - double pos = (cur_sec - display_range.first) / std::max(1.0, max_chart_range); - if (pos < 0 || pos > 0.8) { - display_range.first = std::max(0.0, cur_sec - max_chart_range * 0.1); - } - double max_sec = std::min(std::floor(display_range.first + max_chart_range), can->lastEventSecond()); - display_range.first = std::max(0.0, max_sec - max_chart_range); - display_range.second = display_range.first + max_chart_range; - } else if (cur_sec < zoomed_range.first || cur_sec >= zoomed_range.second) { - // loop in zoommed range - can->seekTo(zoomed_range.first); - } - - const auto &range = is_zoomed ? zoomed_range : display_range; - for (auto c : charts) { - c->updatePlot(cur_sec, range.first, range.second); - } -} - -void ChartsWidget::setMaxChartRange(int value) { - max_chart_range = settings.chart_range = range_slider->value(); - updateToolBar(); - updateState(); -} - -void ChartsWidget::updateToolBar() { - title_label->setText(tr("Charts: %1").arg(charts.size())); - columns_action->setText(tr("Column: %1").arg(column_count)); - range_lb->setText(QString("Range: %1:%2 ").arg(max_chart_range / 60, 2, 10, QLatin1Char('0')).arg(max_chart_range % 60, 2, 10, QLatin1Char('0'))); - range_lb_action->setVisible(!is_zoomed); - range_slider_action->setVisible(!is_zoomed); - reset_zoom_action->setVisible(is_zoomed); - reset_zoom_action->setText(is_zoomed ? tr("Zoomin: %1-%2").arg(zoomed_range.first, 0, 'f', 1).arg(zoomed_range.second, 0, 'f', 1) : ""); - remove_all_btn->setEnabled(!charts.isEmpty()); - dock_btn->setIcon(utils::icon(docking ? "arrow-up-right-square" : "arrow-down-left-square")); - dock_btn->setToolTip(docking ? tr("Undock charts") : tr("Dock charts")); -} - -void ChartsWidget::settingChanged() { - range_slider->setRange(1, settings.max_cached_minutes * 60); - for (auto c : charts) { - c->setFixedHeight(settings.chart_height); - c->setSeriesType((SeriesType)settings.chart_series_type); - } -} - -ChartView *ChartsWidget::findChart(const MessageId &id, const cabana::Signal *sig) { - for (auto c : charts) - if (c->hasSeries(id, sig)) return c; - return nullptr; -} - -ChartView *ChartsWidget::createChart() { - auto chart = new ChartView(this); - chart->setFixedHeight(settings.chart_height); - chart->setMinimumWidth(CHART_MIN_WIDTH); - chart->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); - chart->chart()->setTheme(use_dark_theme ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight); - QObject::connect(chart, &ChartView::remove, [=]() { removeChart(chart); }); - QObject::connect(chart, &ChartView::zoomIn, this, &ChartsWidget::zoomIn); - QObject::connect(chart, &ChartView::zoomReset, this, &ChartsWidget::zoomReset); - QObject::connect(chart, &ChartView::seriesRemoved, this, &ChartsWidget::seriesChanged); - QObject::connect(chart, &ChartView::seriesAdded, this, &ChartsWidget::seriesChanged); - QObject::connect(chart, &ChartView::axisYLabelWidthChanged, this, &ChartsWidget::alignCharts); - charts.push_back(chart); - updateLayout(); - return chart; -} - -void ChartsWidget::showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge) { - ChartView *chart = findChart(id, sig); - if (show && !chart) { - chart = merge && charts.size() > 0 ? charts.back() : createChart(); - chart->addSeries(id, sig); - updateState(); - } else if (!show && chart) { - chart->removeIf([&](auto &s) { return s.msg_id == id && s.sig == sig; }); - } - updateToolBar(); -} - -void ChartsWidget::setColumnCount(int n) { - n = std::clamp(n, 1, MAX_COLUMN_COUNT); - if (column_count != n) { - column_count = settings.chart_column_count = n; - updateToolBar(); - updateLayout(); - } -} - -void ChartsWidget::updateLayout() { - int n = MAX_COLUMN_COUNT; - for (; n > 1; --n) { - if ((n * CHART_MIN_WIDTH + (n - 1) * charts_layout->spacing()) < charts_layout->geometry().width()) break; - } - - bool show_column_cb = n > 1; - columns_action->setVisible(show_column_cb); - - n = std::min(column_count, n); - if (charts.size() != charts_layout->count() || n != current_column_count) { - current_column_count = n; - charts_layout->parentWidget()->setUpdatesEnabled(false); - for (int i = 0; i < charts.size(); ++i) { - charts_layout->addWidget(charts[charts.size() - i - 1], i / n, i % n); - } - QTimer::singleShot(0, [this]() { charts_layout->parentWidget()->setUpdatesEnabled(true); }); - } -} - -void ChartsWidget::resizeEvent(QResizeEvent *event) { - QWidget::resizeEvent(event); - updateLayout(); -} - -void ChartsWidget::newChart() { - SeriesSelector dlg(tr("New Chart"), this); - if (dlg.exec() == QDialog::Accepted) { - auto items = dlg.seletedItems(); - if (!items.isEmpty()) { - auto c = createChart(); - for (auto it : items) { - c->addSeries(it->msg_id, it->sig); - } - } - } -} - -void ChartsWidget::removeChart(ChartView *chart) { - charts.removeOne(chart); - chart->deleteLater(); - updateToolBar(); - alignCharts(); - updateLayout(); - emit seriesChanged(); -} - -void ChartsWidget::removeAll() { - for (auto c : charts) { - c->deleteLater(); - } - charts.clear(); - updateToolBar(); - emit seriesChanged(); -} - -void ChartsWidget::alignCharts() { - int plot_left = 0; - for (auto c : charts) { - plot_left = std::max(plot_left, c->y_label_width); - } - plot_left = std::max((plot_left / 10) * 10 + 10, 50); - for (auto c : charts) { - c->updatePlotArea(plot_left); - } -} - -bool ChartsWidget::eventFilter(QObject *obj, QEvent *event) { - if (obj != this && event->type() == QEvent::Close) { - emit dock_btn->triggered(); - return true; - } - return false; -} - -// ChartView - -ChartView::ChartView(QWidget *parent) : QChartView(nullptr, parent) { - series_type = (SeriesType)settings.chart_series_type; - QChart *chart = new QChart(); - 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(16, 0, 40, 0); - chart->legend()->setShowToolTips(true); - chart->setMargins({0, 0, 0, 0}); - - background = new QGraphicsRectItem(chart); - background->setBrush(QApplication::palette().color(QPalette::Base)); - background->setPen(Qt::NoPen); - background->setZValue(chart->zValue() - 1); - - setChart(chart); - - createToolButtons(); - setRenderHint(QPainter::Antialiasing); - // TODO: enable zoomIn/seekTo in live streaming mode. - setRubberBand(can->liveStreaming() ? QChartView::NoRubberBand : QChartView::HorizontalRubberBand); - setMouseTracking(true); - - QObject::connect(dbc(), &DBCManager::signalRemoved, this, &ChartView::signalRemoved); - QObject::connect(dbc(), &DBCManager::signalUpdated, this, &ChartView::signalUpdated); - QObject::connect(dbc(), &DBCManager::msgRemoved, this, &ChartView::msgRemoved); - QObject::connect(dbc(), &DBCManager::msgUpdated, this, &ChartView::msgUpdated); -} - -void ChartView::createToolButtons() { - move_icon = new QGraphicsPixmapItem(utils::icon("grip-horizontal"), chart()); - move_icon->setToolTip(tr("Drag and drop to combine charts")); - - QToolButton *remove_btn = toolButton("x", tr("Remove Chart")); - close_btn_proxy = new QGraphicsProxyWidget(chart()); - close_btn_proxy->setWidget(remove_btn); - close_btn_proxy->setZValue(chart()->zValue() + 11); - - // series types - QMenu *menu = new QMenu(this); - auto change_series_group = new QActionGroup(menu); - change_series_group->setExclusive(true); - QStringList types{tr("line"), tr("Step Line"), tr("Scatter")}; - for (int i = 0; i < types.size(); ++i) { - QAction *act = new QAction(types[i], change_series_group); - act->setData(i); - act->setCheckable(true); - act->setChecked(i == (int)series_type); - menu->addAction(act); - } - menu->addSeparator(); - menu->addAction(tr("Manage series"), this, &ChartView::manageSeries); - - QToolButton *manage_btn = toolButton("list", ""); - manage_btn->setMenu(menu); - manage_btn->setPopupMode(QToolButton::InstantPopup); - manage_btn_proxy = new QGraphicsProxyWidget(chart()); - manage_btn_proxy->setWidget(manage_btn); - manage_btn_proxy->setZValue(chart()->zValue() + 11); - - QObject::connect(remove_btn, &QToolButton::clicked, this, &ChartView::remove); - QObject::connect(change_series_group, &QActionGroup::triggered, [this](QAction *action) { - setSeriesType((SeriesType)action->data().toInt()); - }); -} - -void ChartView::addSeries(const MessageId &msg_id, const cabana::Signal *sig) { - if (hasSeries(msg_id, sig)) return; - - QXYSeries *series = createSeries(series_type, getColor(sig)); - sigs.push_back({.msg_id = msg_id, .sig = sig, .series = series}); - updateTitle(); - updateSeries(sig); - updateSeriesPoints(); - emit seriesAdded(msg_id, sig); -} - -bool ChartView::hasSeries(const MessageId &msg_id, const cabana::Signal *sig) const { - return std::any_of(sigs.begin(), sigs.end(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; }); -} - -void ChartView::removeIf(std::function predicate) { - int prev_size = sigs.size(); - for (auto it = sigs.begin(); it != sigs.end(); /**/) { - if (predicate(*it)) { - chart()->removeSeries(it->series); - it->series->deleteLater(); - auto msg_id = it->msg_id; - auto sig = it->sig; - it = sigs.erase(it); - emit seriesRemoved(msg_id, sig); - } else { - ++it; - } - } - if (sigs.empty()) { - emit remove(); - } else if (sigs.size() != prev_size) { - updateAxisY(); - } -} - -void ChartView::signalUpdated(const cabana::Signal *sig) { - if (std::any_of(sigs.begin(), sigs.end(), [=](auto &s) { return s.sig == sig; })) { - updateTitle(); - // TODO: don't update series if only name changed. - updateSeries(sig); - } -} - -void ChartView::msgUpdated(MessageId id) { - if (std::any_of(sigs.begin(), sigs.end(), [=](auto &s) { return s.msg_id == id; })) - updateTitle(); -} - -void ChartView::manageSeries() { - SeriesSelector dlg(tr("Mange Chart"), this); - for (auto &s : sigs) { - dlg.addSelected(s.msg_id, s.sig); - } - if (dlg.exec() == QDialog::Accepted) { - auto items = dlg.seletedItems(); - for (auto s : items) { - addSeries(s->msg_id, s->sig); - } - removeIf([&](auto &s) { - return std::none_of(items.cbegin(), items.cend(), [&](auto &it) { return s.msg_id == it->msg_id && s.sig == it->sig; }); - }); - } -} - -void ChartView::resizeEvent(QResizeEvent *event) { - updatePlotArea(align_to); - int top_margin = style()->pixelMetric(QStyle::PM_LayoutTopMargin); - int spacing = style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing); - int x = event->size().width() - close_btn_proxy->size().width() - style()->pixelMetric(QStyle::PM_LayoutRightMargin); - close_btn_proxy->setPos(x, top_margin); - manage_btn_proxy->setPos(x - manage_btn_proxy->size().width() - spacing, top_margin); - move_icon->setPos(style()->pixelMetric(QStyle::PM_LayoutLeftMargin), top_margin); - QChartView::resizeEvent(event); -} - -void ChartView::updatePlotArea(int left) { - QRect r = rect(); - if (align_to != left || r != background->rect()) { - align_to = left; - background->setRect(r); - chart()->legend()->setGeometry(QRect(r.left(), r.top(), r.width(), 45)); - chart()->setPlotArea(QRect(align_to, r.top() + 45, r.width() - align_to - 36, r.height() - 80)); - chart()->layout()->invalidate(); - } -} - -void ChartView::updateTitle() { - for (QLegendMarker *marker : chart()->legend()->markers()) { - QObject::connect(marker, &QLegendMarker::clicked, this, &ChartView::handleMarkerClicked, Qt::UniqueConnection); - } - for (auto &s : sigs) { - auto decoration = s.series->isVisible() ? "none" : "line-through"; - s.series->setName(QString("%2 %3 %4").arg(decoration, s.sig->name, msgName(s.msg_id), s.msg_id.toString())); - } -} - -void ChartView::updatePlot(double cur, double min, double max) { - cur_sec = cur; - if (min != axis_x->min() || max != axis_x->max()) { - axis_x->setRange(min, max); - updateAxisY(); - updateSeriesPoints(); - } - - scene()->invalidate({}, QGraphicsScene::ForegroundLayer); -} - -void ChartView::updateSeriesPoints() { - // Show points when zoomed in enough - for (auto &s : sigs) { - auto begin = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->min(), xLessThan); - auto end = std::lower_bound(begin, s.vals.end(), axis_x->max(), xLessThan); - - int num_points = std::max(end - begin, 1); - int pixels_per_point = width() / num_points; - - if (series_type == SeriesType::Scatter) { - ((QScatterSeries *)s.series)->setMarkerSize(std::clamp(pixels_per_point / 3, 2, 8)); - } else { - s.series->setPointsVisible(pixels_per_point > 20); - } - } -} - -void ChartView::updateSeries(const cabana::Signal *sig) { - for (auto &s : sigs) { - if (!sig || s.sig == sig) { - if (!can->liveStreaming()) { - s.vals.clear(); - s.step_vals.clear(); - s.last_value_mono_time = 0; - } - s.series->setColor(getColor(s.sig)); - - auto msgs = can->events().at(s.msg_id); - auto first = std::upper_bound(msgs.cbegin(), msgs.cend(), CanEvent{.mono_time=s.last_value_mono_time}); - int new_size = std::max(s.vals.size() + std::distance(first, msgs.cend()), settings.max_cached_minutes * 60 * 100); - if (s.vals.capacity() <= new_size) { - s.vals.reserve(new_size * 2); - s.step_vals.reserve(new_size * 4); - } - - const double route_start_time = can->routeStartTime(); - for (auto end = msgs.cend(); first != end; ++first) { - double value = get_raw_value(first->dat, first->size, *s.sig); - double ts = first->mono_time / 1e9 - route_start_time; // seconds - s.vals.append({ts, value}); - if (!s.step_vals.empty()) { - s.step_vals.append({ts, s.step_vals.back().y()}); - } - s.step_vals.append({ts, value}); - s.last_value_mono_time = first->mono_time; - } - if (!can->liveStreaming()) { - s.segment_tree.build(s.vals); - } - s.series->replace(series_type == SeriesType::StepLine ? s.step_vals : s.vals); - } - } - updateAxisY(); -} - -// auto zoom on yaxis -void ChartView::updateAxisY() { - if (sigs.isEmpty()) return; - - double min = std::numeric_limits::max(); - double max = std::numeric_limits::lowest(); - QString unit = sigs[0].sig->unit; - - for (auto &s : sigs) { - if (!s.series->isVisible()) continue; - - // Only show unit when all signals have the same unit - if (unit != s.sig->unit) { - unit.clear(); - } - - auto first = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->min(), xLessThan); - auto last = std::lower_bound(first, s.vals.end(), axis_x->max(), xLessThan); - if (can->liveStreaming()) { - for (auto it = first; it != last; ++it) { - if (it->y() < min) min = it->y(); - if (it->y() > max) max = it->y(); - } - } else { - auto [min_y, max_y] = s.segment_tree.minmax(std::distance(s.vals.begin(), first), std::distance(s.vals.begin(), last)); - min = std::min(min, min_y); - max = std::max(max, max_y); - } - } - if (min == std::numeric_limits::max()) min = 0; - if (max == std::numeric_limits::lowest()) max = 0; - - if (axis_y->titleText() != unit) { - axis_y->setTitleText(unit); - y_label_width = 0;// recalc width - } - - double delta = std::abs(max - min) < 1e-3 ? 1 : (max - min) * 0.05; - auto [min_y, max_y, tick_count] = getNiceAxisNumbers(min - delta, max + delta, axis_y->tickCount()); - if (min_y != axis_y->min() || max_y != axis_y->max() || y_label_width == 0) { - axis_y->setRange(min_y, max_y); - axis_y->setTickCount(tick_count); - - QFontMetrics fm(axis_y->labelsFont()); - int n = qMax(int(-qFloor(std::log10((max_y - min_y) / (tick_count - 1)))), 0) + 1; - int title_spacing = axis_y->titleText().isEmpty() ? 0 : 20; - y_label_width = title_spacing + qMax(fm.width(QString::number(min_y, 'f', n)), fm.width(QString::number(max_y, 'f', n))) + 15; // left margin 15 - axis_y->setLabelFormat(QString("%.%1f").arg(n)); - emit axisYLabelWidthChanged(y_label_width); - } -} - -std::tuple ChartView::getNiceAxisNumbers(qreal min, qreal max, int tick_count) { - qreal range = niceNumber((max - min), true); // range with ceiling - qreal step = niceNumber(range / (tick_count - 1), false); - min = qFloor(min / step); - max = qCeil(max / step); - tick_count = int(max - min) + 1; - return {min * step, max * step, tick_count}; -} - -// nice numbers can be expressed as form of 1*10^n, 2* 10^n or 5*10^n -qreal ChartView::niceNumber(qreal x, bool ceiling) { - qreal z = qPow(10, qFloor(std::log10(x))); //find corresponding number of the form of 10^n than is smaller than x - qreal q = x / z; //q<10 && q>=1; - if (ceiling) { - if (q <= 1.0) q = 1; - else if (q <= 2.0) q = 2; - else if (q <= 5.0) q = 5; - else q = 10; - } else { - if (q < 1.5) q = 1; - else if (q < 3.0) q = 2; - else if (q < 7.0) q = 5; - else q = 10; - } - return q * z; -} - -void ChartView::leaveEvent(QEvent *event) { - clearTrackPoints(); - scene()->update(); - QChartView::leaveEvent(event); -} - -void ChartView::mousePressEvent(QMouseEvent *event) { - if (event->button() == Qt::LeftButton && move_icon->sceneBoundingRect().contains(event->pos())) { - QMimeData *mimeData = new QMimeData; - mimeData->setData(mime_type, QByteArray::number((qulonglong)this)); - QDrag *drag = new QDrag(this); - drag->setMimeData(mimeData); - drag->setPixmap(grab()); - drag->setHotSpot(event->pos()); - Qt::DropAction dropAction = drag->exec(Qt::CopyAction | Qt::MoveAction, Qt::MoveAction); - if (dropAction == Qt::MoveAction) { - return; - } - } else if (event->button() == Qt::LeftButton && QApplication::keyboardModifiers().testFlag(Qt::ShiftModifier)) { - if (!can->liveStreaming()) { - // Save current playback state when scrubbing - resume_after_scrub = !can->isPaused(); - if (resume_after_scrub) { - can->pause(true); - } - - is_scrubbing = true; - } - } else { - QChartView::mousePressEvent(event); - } -} - -void ChartView::mouseReleaseEvent(QMouseEvent *event) { - auto rubber = findChild(); - if (event->button() == Qt::LeftButton && rubber && rubber->isVisible()) { - rubber->hide(); - QRectF rect = rubber->geometry().normalized(); - double min = chart()->mapToValue(rect.topLeft()).x(); - double max = chart()->mapToValue(rect.bottomRight()).x(); - - // Prevent zooming/seeking past the end of the route - min = std::clamp(min, 0., can->totalSeconds()); - max = std::clamp(max, 0., can->totalSeconds()); - - double min_rounded = std::floor(min * 10.0) / 10.0; - double max_rounded = std::floor(max * 10.0) / 10.0; - if (rubber->width() <= 0) { - // no rubber dragged, seek to mouse position - can->seekTo(min); - } else if ((max_rounded - min_rounded) >= 0.5) { - // zoom in if selected range is greater than 0.5s - emit zoomIn(min_rounded, max_rounded); - } else { - scene()->invalidate({}, QGraphicsScene::ForegroundLayer); - } - event->accept(); - } else if (!can->liveStreaming() && event->button() == Qt::RightButton) { - emit zoomReset(); - event->accept(); - } else { - QGraphicsView::mouseReleaseEvent(event); - } - - // Resume playback if we were scrubbing - is_scrubbing = false; - if (resume_after_scrub) { - can->pause(false); - resume_after_scrub = false; - } -} - -void ChartView::mouseMoveEvent(QMouseEvent *ev) { - // Scrubbing - if (is_scrubbing && QApplication::keyboardModifiers().testFlag(Qt::ShiftModifier)) { - if (chart()->plotArea().contains(ev->pos())) { - double t = chart()->mapToValue(ev->pos()).x(); - // Prevent seeking past the end of the route - t = std::clamp(t, 0., can->totalSeconds()); - can->seekTo(t); - } - return; - } - - auto rubber = findChild(); - bool is_zooming = rubber && rubber->isVisible(); - const auto plot_area = chart()->plotArea(); - clearTrackPoints(); - - if (!is_zooming && plot_area.contains(ev->pos())) { - QStringList text_list; - const double sec = chart()->mapToValue(ev->pos()).x(); - qreal x = -1; - for (auto &s : sigs) { - if (!s.series->isVisible()) continue; - - // use reverse iterator to find last item <= sec. - double value = 0; - auto it = std::lower_bound(s.vals.rbegin(), s.vals.rend(), sec, [](auto &p, double x) { return p.x() > x; }); - if (it != s.vals.rend() && it->x() >= axis_x->min()) { - value = it->y(); - s.track_pt = chart()->mapToPosition(*it); - x = std::max(x, s.track_pt.x()); - } - text_list.push_back(QString("%2: %3") - .arg(s.series->color().name(), s.sig->name, - s.track_pt.isNull() ? "--" : QString::number(value))); - } - if (x < 0) { - x = ev->pos().x(); - } - text_list.push_front(QString::number(chart()->mapToValue({x, 0}).x(), 'f', 3)); - QPointF tooltip_pt(x + 12, plot_area.top() - 20); - QToolTip::showText(mapToGlobal(tooltip_pt.toPoint()), text_list.join("
"), this, plot_area.toRect()); - scene()->invalidate({}, QGraphicsScene::ForegroundLayer); - } else { - QToolTip::hideText(); - } - - QChartView::mouseMoveEvent(ev); - if (is_zooming) { - QRect rubber_rect = rubber->geometry(); - rubber_rect.setLeft(std::max(rubber_rect.left(), (int)plot_area.left())); - rubber_rect.setRight(std::min(rubber_rect.right(), (int)plot_area.right())); - if (rubber_rect != rubber->geometry()) { - rubber->setGeometry(rubber_rect); - } - scene()->invalidate({}, QGraphicsScene::ForegroundLayer); - } -} - -void ChartView::dragMoveEvent(QDragMoveEvent *event) { - if (event->mimeData()->hasFormat(mime_type)) { - event->setDropAction(event->source() == this ? Qt::MoveAction : Qt::CopyAction); - event->accept(); - } else { - event->ignore(); - } -} - -void ChartView::dropEvent(QDropEvent *event) { - if (event->mimeData()->hasFormat(mime_type)) { - if (event->source() == this) { - event->setDropAction(Qt::MoveAction); - event->accept(); - } else { - ChartView *source_chart = (ChartView *)event->source(); - for (auto &s : source_chart->sigs) { - addSeries(s.msg_id, s.sig); - } - emit source_chart->remove(); - event->acceptProposedAction(); - } - } else { - event->ignore(); - } -} - -void ChartView::drawForeground(QPainter *painter, const QRectF &rect) { - // draw time line - qreal x = chart()->mapToPosition(QPointF{cur_sec, 0}).x(); - x = std::clamp(x, chart()->plotArea().left(), chart()->plotArea().right()); - qreal y1 = chart()->plotArea().top() - 2; - qreal y2 = chart()->plotArea().bottom() + 2; - painter->setPen(QPen(chart()->titleBrush().color(), 2)); - painter->drawLine(QPointF{x, y1}, QPointF{x, y2}); - - // draw track points - painter->setPen(Qt::NoPen); - qreal track_line_x = -1; - for (auto &s : sigs) { - if (!s.track_pt.isNull() && s.series->isVisible()) { - painter->setBrush(s.series->color().darker(125)); - painter->drawEllipse(s.track_pt, 5.5, 5.5); - track_line_x = std::max(track_line_x, s.track_pt.x()); - } - } - if (track_line_x > 0) { - painter->setPen(QPen(Qt::darkGray, 1, Qt::DashLine)); - painter->drawLine(QPointF{track_line_x, y1}, QPointF{track_line_x, y2}); - } - - // paint points. OpenGL mode lacks certain features (such as showing points) - painter->setPen(Qt::NoPen); - for (auto &s : sigs) { - if (s.series->useOpenGL() && s.series->isVisible() && s.series->pointsVisible()) { - auto first = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->min(), xLessThan); - auto last = std::lower_bound(first, s.vals.end(), axis_x->max(), xLessThan); - for (auto it = first; it != last; ++it) { - painter->setBrush(s.series->color()); - painter->drawEllipse(chart()->mapToPosition(*it), 4, 4); - } - } - } - - // paint zoom range - auto rubber = findChild(); - if (rubber && rubber->isVisible() && rubber->width() > 1) { - painter->setPen(Qt::white); - auto rubber_rect = rubber->geometry().normalized(); - for (const auto &pt : {rubber_rect.bottomLeft(), rubber_rect.bottomRight()}) { - QString sec = QString::number(chart()->mapToValue(pt).x(), 'f', 1); - // ChartAxisElement's padding is 4 (https://codebrowser.dev/qt5/qtcharts/src/charts/axis/chartaxiselement_p.h.html) - auto r = painter->fontMetrics().boundingRect(sec).adjusted(-6, -4, 6, 4); - pt == rubber_rect.bottomLeft() ? r.moveTopRight(pt + QPoint{0, 2}) : r.moveTopLeft(pt + QPoint{0, 2}); - painter->fillRect(r, Qt::gray); - painter->drawText(r, Qt::AlignCenter, sec); - } - } -} - -QXYSeries *ChartView::createSeries(SeriesType type, QColor color) { - QXYSeries *series = nullptr; - if (type == SeriesType::Line) { - series = new QLineSeries(this); - chart()->legend()->setMarkerShape(QLegend::MarkerShapeRectangle); - } else if (type == SeriesType::StepLine) { - series = new QLineSeries(this); - chart()->legend()->setMarkerShape(QLegend::MarkerShapeFromSeries); - } else { - series = new QScatterSeries(this); - chart()->legend()->setMarkerShape(QLegend::MarkerShapeCircle); - } - series->setColor(color); - // TODO: Due to a bug in CameraWidget the camera frames - // are drawn instead of the graphs on MacOS. Re-enable OpenGL when fixed -#ifndef __APPLE__ - series->setUseOpenGL(true); - // Qt doesn't properly apply device pixel ratio in OpenGL mode - QPen pen = series->pen(); - pen.setWidth(2.0 * qApp->devicePixelRatio()); - series->setPen(pen); -#endif - chart()->addSeries(series); - series->attachAxis(axis_x); - series->attachAxis(axis_y); - return series; -} - -void ChartView::setSeriesType(SeriesType type) { - if (type != series_type) { - series_type = type; - for (auto &s : sigs) { - chart()->removeSeries(s.series); - s.series->deleteLater(); - } - for (auto &s : sigs) { - auto series = createSeries(series_type, getColor(s.sig)); - series->replace(series_type == SeriesType::StepLine ? s.step_vals : s.vals); - s.series = series; - } - updateSeriesPoints(); - updateTitle(); - } -} - -void ChartView::handleMarkerClicked() { - auto marker = qobject_cast(sender()); - Q_ASSERT(marker); - if (sigs.size() > 1) { - auto series = marker->series(); - series->setVisible(!series->isVisible()); - marker->setVisible(true); - updateAxisY(); - updateTitle(); - } -} - -// SeriesSelector - -SeriesSelector::SeriesSelector(QString title, QWidget *parent) : QDialog(parent) { - setWindowTitle(title); - QGridLayout *main_layout = new QGridLayout(this); - - // left column - main_layout->addWidget(new QLabel(tr("Available Signals")), 0, 0); - main_layout->addWidget(msgs_combo = new QComboBox(this), 1, 0); - msgs_combo->setEditable(true); - msgs_combo->lineEdit()->setPlaceholderText(tr("Select a msg...")); - msgs_combo->setInsertPolicy(QComboBox::NoInsert); - msgs_combo->completer()->setCompletionMode(QCompleter::PopupCompletion); - msgs_combo->completer()->setFilterMode(Qt::MatchContains); - - main_layout->addWidget(available_list = new QListWidget(this), 2, 0); - - // buttons - QVBoxLayout *btn_layout = new QVBoxLayout(); - QPushButton *add_btn = new QPushButton(utils::icon("chevron-right"), "", this); - add_btn->setEnabled(false); - QPushButton *remove_btn = new QPushButton(utils::icon("chevron-left"), "", this); - remove_btn->setEnabled(false); - btn_layout->addStretch(0); - btn_layout->addWidget(add_btn); - btn_layout->addWidget(remove_btn); - btn_layout->addStretch(0); - main_layout->addLayout(btn_layout, 0, 1, 3, 1); - - // right column - main_layout->addWidget(new QLabel(tr("Selected Signals")), 0, 2); - main_layout->addWidget(selected_list = new QListWidget(this), 1, 2, 2, 1); - - 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())); - } - } - msgs_combo->model()->sort(0); - msgs_combo->setCurrentIndex(-1); - - QObject::connect(msgs_combo, qOverload(&QComboBox::currentIndexChanged), this, &SeriesSelector::updateAvailableList); - QObject::connect(available_list, &QListWidget::currentRowChanged, [=](int row) { add_btn->setEnabled(row != -1); }); - QObject::connect(selected_list, &QListWidget::currentRowChanged, [=](int row) { remove_btn->setEnabled(row != -1); }); - QObject::connect(available_list, &QListWidget::itemDoubleClicked, this, &SeriesSelector::add); - QObject::connect(selected_list, &QListWidget::itemDoubleClicked, this, &SeriesSelector::remove); - QObject::connect(add_btn, &QPushButton::clicked, [this]() { if (auto item = available_list->currentItem()) add(item); }); - QObject::connect(remove_btn, &QPushButton::clicked, [this]() { if (auto item = selected_list->currentItem()) remove(item);}); - QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); - QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); -} - -void SeriesSelector::add(QListWidgetItem *item) { - auto it = (ListItem *)item; - addItemToList(selected_list, it->msg_id, it->sig, true); - delete item; -} - -void SeriesSelector::remove(QListWidgetItem *item) { - auto it = (ListItem *)item; - if (it->msg_id == msgs_combo->currentData().value()) { - addItemToList(available_list, it->msg_id, it->sig); - } - delete item; -} - -void SeriesSelector::updateAvailableList(int index) { - if (index == -1) return; - available_list->clear(); - MessageId msg_id = msgs_combo->itemData(index).value(); - auto selected_items = seletedItems(); - for (auto s : dbc()->msg(msg_id)->getSignals()) { - bool is_selected = std::any_of(selected_items.begin(), selected_items.end(), [=, sig=s](auto it) { return it->msg_id == msg_id && it->sig == sig; }); - if (!is_selected) { - addItemToList(available_list, msg_id, s); - } - } -} - -void SeriesSelector::addItemToList(QListWidget *parent, const MessageId id, const cabana::Signal *sig, bool show_msg_name) { - QString text = QString(" %1").arg(getColor(sig).name(), sig->name); - if (show_msg_name) text += QString(" %0 %1").arg(msgName(id), id.toString()); - - QLabel *label = new QLabel(text); - label->setContentsMargins(5, 0, 5, 0); - auto new_item = new ListItem(id, sig, parent); - new_item->setSizeHint(label->sizeHint()); - parent->setItemWidget(new_item, label); -} - -QList SeriesSelector::seletedItems() { - QList ret; - for (int i = 0; i < selected_list->count(); ++i) ret.push_back((ListItem *)selected_list->item(i)); - return ret; -} diff --git a/tools/cabana/chartswidget.h b/tools/cabana/chartswidget.h deleted file mode 100644 index c7470a6f12..0000000000 --- a/tools/cabana/chartswidget.h +++ /dev/null @@ -1,180 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/dbcmanager.h" -#include "tools/cabana/streams/abstractstream.h" -using namespace QtCharts; - -const int CHART_MIN_WIDTH = 300; - -enum class SeriesType { - Line = 0, - StepLine, - Scatter -}; - -class ChartView : public QChartView { - Q_OBJECT - -public: - ChartView(QWidget *parent = nullptr); - void addSeries(const MessageId &msg_id, const cabana::Signal *sig); - bool hasSeries(const MessageId &msg_id, const cabana::Signal *sig) const; - void updateSeries(const cabana::Signal *sig = nullptr); - void updatePlot(double cur, double min, double max); - void setSeriesType(SeriesType type); - void updatePlotArea(int left); - - struct SigItem { - MessageId msg_id; - const cabana::Signal *sig = nullptr; - QXYSeries *series = nullptr; - QVector vals; - QVector step_vals; - uint64_t last_value_mono_time = 0; - QPointF track_pt{}; - SegmentTree segment_tree; - }; - -signals: - void seriesRemoved(const MessageId &id, const cabana::Signal *sig); - void seriesAdded(const MessageId &id, const cabana::Signal *sig); - void zoomIn(double min, double max); - void zoomReset(); - void remove(); - void axisYLabelWidthChanged(int w); - -private slots: - void signalUpdated(const cabana::Signal *sig); - void manageSeries(); - void handleMarkerClicked(); - void msgUpdated(MessageId id); - void msgRemoved(MessageId id) { removeIf([=](auto &s) { return s.msg_id == id; }); } - void signalRemoved(const cabana::Signal *sig) { removeIf([=](auto &s) { return s.sig == sig; }); } - -private: - void createToolButtons(); - void mousePressEvent(QMouseEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override; - void mouseMoveEvent(QMouseEvent *ev) override; - void dragMoveEvent(QDragMoveEvent *event) override; - void dropEvent(QDropEvent *event) override; - void leaveEvent(QEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - QSize sizeHint() const override { return {CHART_MIN_WIDTH, settings.chart_height}; } - void updateAxisY(); - void updateTitle(); - void drawForeground(QPainter *painter, const QRectF &rect) override; - std::tuple getNiceAxisNumbers(qreal min, qreal max, int tick_count); - qreal niceNumber(qreal x, bool ceiling); - QXYSeries *createSeries(SeriesType type, QColor color); - void updateSeriesPoints(); - void removeIf(std::function predicate); - inline void clearTrackPoints() { for (auto &s : sigs) s.track_pt = {}; } - - int y_label_width = 0; - int align_to = 0; - QValueAxis *axis_x; - QValueAxis *axis_y; - QGraphicsPixmapItem *move_icon; - QGraphicsProxyWidget *close_btn_proxy; - QGraphicsProxyWidget *manage_btn_proxy; - QGraphicsRectItem *background; - QList sigs; - double cur_sec = 0; - const QString mime_type = "application/x-cabanachartview"; - SeriesType series_type = SeriesType::Line; - bool is_scrubbing = false; - bool resume_after_scrub = false; - friend class ChartsWidget; - }; - -class ChartsWidget : public QFrame { - Q_OBJECT - -public: - ChartsWidget(QWidget *parent = nullptr); - void showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge); - inline bool hasSignal(const MessageId &id, const cabana::Signal *sig) { return findChart(id, sig) != nullptr; } - -public slots: - void setColumnCount(int n); - void removeAll(); - -signals: - void dock(bool floating); - void rangeChanged(double min, double max, bool is_zommed); - void seriesChanged(); - -private: - void resizeEvent(QResizeEvent *event) override; - void alignCharts(); - void newChart(); - ChartView * createChart(); - void removeChart(ChartView *chart); - void eventsMerged(); - void updateState(); - void zoomIn(double min, double max); - void zoomReset(); - void updateToolBar(); - void setMaxChartRange(int value); - void updateLayout(); - void settingChanged(); - bool eventFilter(QObject *obj, QEvent *event) override; - ChartView *findChart(const MessageId &id, const cabana::Signal *sig); - - QLabel *title_label; - QLabel *range_lb; - LogSlider *range_slider; - QAction *range_lb_action; - QAction *range_slider_action; - bool docking = true; - QAction *dock_btn; - QAction *reset_zoom_action; - QAction *remove_all_btn; - QGridLayout *charts_layout; - QList charts; - uint32_t max_chart_range = 0; - bool is_zoomed = false; - std::pair display_range; - std::pair zoomed_range; - bool use_dark_theme = false; - QAction *columns_action; - int column_count = 1; - int current_column_count = 0; -}; - -class SeriesSelector : public QDialog { -public: - struct ListItem : public QListWidgetItem { - ListItem(const MessageId &msg_id, const cabana::Signal *sig, QListWidget *parent) : msg_id(msg_id), sig(sig), QListWidgetItem(parent) {} - MessageId msg_id; - const cabana::Signal *sig; - }; - - SeriesSelector(QString title, QWidget *parent); - QList seletedItems(); - inline void addSelected(const MessageId &id, const cabana::Signal *sig) { addItemToList(selected_list, id, sig, true); } - -private: - void updateAvailableList(int index); - void addItemToList(QListWidget *parent, const MessageId id, const cabana::Signal *sig, bool show_msg_name = false); - void add(QListWidgetItem *item); - void remove(QListWidgetItem *item); - - QComboBox *msgs_combo; - QListWidget *available_list; - QListWidget *selected_list; -}; diff --git a/tools/cabana/commands.h b/tools/cabana/commands.h index 2a5f40d584..84d1a6e2a1 100644 --- a/tools/cabana/commands.h +++ b/tools/cabana/commands.h @@ -3,7 +3,7 @@ #include #include -#include "tools/cabana/dbcmanager.h" +#include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" class EditMsgCommand : public QUndoCommand { diff --git a/tools/cabana/dbc.cc b/tools/cabana/dbc/dbc.cc similarity index 98% rename from tools/cabana/dbc.cc rename to tools/cabana/dbc/dbc.cc index 38002af06c..46302ad789 100644 --- a/tools/cabana/dbc.cc +++ b/tools/cabana/dbc/dbc.cc @@ -1,4 +1,4 @@ -#include "tools/cabana/dbc.h" +#include "tools/cabana/dbc/dbc.h" #include "tools/cabana/util.h" uint qHash(const MessageId &item) { diff --git a/tools/cabana/dbc.h b/tools/cabana/dbc/dbc.h similarity index 97% rename from tools/cabana/dbc.h rename to tools/cabana/dbc/dbc.h index 701908112f..8b1e5a16e9 100644 --- a/tools/cabana/dbc.h +++ b/tools/cabana/dbc/dbc.h @@ -11,8 +11,8 @@ const QString UNTITLED = "untitled"; struct MessageId { - uint8_t source; - uint32_t address; + uint8_t source = 0; + uint32_t address = 0; QString toString() const { return QString("%1:%2").arg(source).arg(address, 1, 16); diff --git a/tools/cabana/dbcmanager.cc b/tools/cabana/dbc/dbcfile.cc similarity index 51% rename from tools/cabana/dbcmanager.cc rename to tools/cabana/dbc/dbcfile.cc index 49aac5c18b..0534ce39ff 100644 --- a/tools/cabana/dbcmanager.cc +++ b/tools/cabana/dbc/dbcfile.cc @@ -1,58 +1,235 @@ -#include "tools/cabana/dbcmanager.h" +#include "tools/cabana/dbc/dbcfile.h" + #include #include +#include #include #include #include #include #include -bool DBCManager::open(const QString &dbc_file_name, QString *error) { - QString opendbc_file_path = QString("%1/%2.dbc").arg(OPENDBC_FILE_PATH, dbc_file_name); - QFile file(opendbc_file_path); + +DBCFile::DBCFile(const QString &dbc_file_name, QObject *parent) : QObject(parent) { + QFile file(dbc_file_name); if (file.open(QIODevice::ReadOnly)) { - return open(dbc_file_name, file.readAll(), error); - } - return false; -} - -bool DBCManager::open(const QString &name, const QString &content, QString *error) { - try { - std::istringstream stream(content.toStdString()); - auto dbc = const_cast(dbc_parse_from_stream(name.toStdString(), stream)); - msgs.clear(); - for (auto &msg : dbc->msgs) { - auto &m = msgs[msg.address]; - m.name = msg.name.c_str(); - m.size = msg.size; - for (auto &s : msg.sigs) { - m.sigs.push_back({}); - auto &sig = m.sigs.last(); - sig.name = s.name.c_str(); - sig.start_bit = s.start_bit; - sig.msb = s.msb; - sig.lsb = s.lsb; - sig.size = s.size; - sig.is_signed = s.is_signed; - sig.factor = s.factor; - sig.offset = s.offset; - sig.is_little_endian = s.is_little_endian; - sig.updatePrecision(); - } + name_ = QFileInfo(dbc_file_name).baseName(); + + // Remove auto save file extension + if (dbc_file_name.endsWith(AUTO_SAVE_EXTENSION)) { + filename = dbc_file_name.left(dbc_file_name.length() - AUTO_SAVE_EXTENSION.length()); + } else { + filename = dbc_file_name; + } + open(file.readAll()); + } else { + throw std::runtime_error("Failed to open file."); + } +} + +DBCFile::DBCFile(const QString &name, const QString &content, QObject *parent) : QObject(parent), name_(name), filename("") { + // Open from clipboard + open(content); +} + +void DBCFile::open(const QString &content) { + std::istringstream stream(content.toStdString()); + auto dbc = const_cast(dbc_parse_from_stream(name_.toStdString(), stream)); + msgs.clear(); + for (auto &msg : dbc->msgs) { + auto &m = msgs[msg.address]; + m.name = msg.name.c_str(); + m.size = msg.size; + for (auto &s : msg.sigs) { + m.sigs.push_back({}); + auto &sig = m.sigs.last(); + sig.name = s.name.c_str(); + sig.start_bit = s.start_bit; + sig.msb = s.msb; + sig.lsb = s.lsb; + sig.size = s.size; + sig.is_signed = s.is_signed; + sig.factor = s.factor; + sig.offset = s.offset; + sig.is_little_endian = s.is_little_endian; + sig.updatePrecision(); } - parseExtraInfo(content); - name_ = name; - emit DBCFileChanged(); - delete dbc; - } catch (std::exception &e) { - if (error) *error = e.what(); + } + parseExtraInfo(content); + + delete dbc; +} + +bool DBCFile::save() { + assert (!filename.isEmpty()); + if (writeContents(filename)) { + cleanupAutoSaveFile(); + return true; + } else { + return false; + } +} + +bool DBCFile::saveAs(const QString &new_filename) { + filename = new_filename; + return save(); +} + +bool DBCFile::autoSave() { + if (!filename.isEmpty()) { + return writeContents(filename + AUTO_SAVE_EXTENSION); + } else { return false; } - return true; } -void DBCManager::parseExtraInfo(const QString &content) { +void DBCFile::cleanupAutoSaveFile() { + if (!filename.isEmpty()) { + QFile::remove(filename + AUTO_SAVE_EXTENSION); + } +} + +bool DBCFile::writeContents(const QString &fn) { + QFile file(fn); + if (file.open(QIODevice::WriteOnly)) { + file.write(generateDBC().toUtf8()); + return true; + } else { + return false; + } +} + +cabana::Signal *DBCFile::addSignal(const MessageId &id, const cabana::Signal &sig) { + if (auto m = const_cast(msg(id.address))) { + m->sigs.push_back(sig); + return &m->sigs.last(); + } + + return nullptr; +} + + cabana::Signal *DBCFile::updateSignal(const MessageId &id, const QString &sig_name, const cabana::Signal &sig) { + if (auto m = const_cast(msg(id))) { + if (auto s = (cabana::Signal *)m->sig(sig_name)) { + *s = sig; + return s; + } + } + + return nullptr; +} + +cabana::Signal *DBCFile::getSignal(const MessageId &id, const QString &sig_name) { + if (auto m = const_cast(msg(id))) { + auto it = std::find_if(m->sigs.begin(), m->sigs.end(), [&](auto &s) { return s.name == sig_name; }); + if (it != m->sigs.end()) { + return &(*it); + } + } + return nullptr; + } + +void DBCFile::removeSignal(const MessageId &id, const QString &sig_name) { + if (auto m = const_cast(msg(id))) { + auto it = std::find_if(m->sigs.begin(), m->sigs.end(), [&](auto &s) { return s.name == sig_name; }); + if (it != m->sigs.end()) { + m->sigs.erase(it); + } + } +} + +void DBCFile::updateMsg(const MessageId &id, const QString &name, uint32_t size) { + auto &m = msgs[id.address]; + m.name = name; + m.size = size; +} + +void DBCFile::removeMsg(const MessageId &id) { + msgs.erase(id.address); +} + +QString DBCFile::newMsgName(const MessageId &id) { + return QString("NEW_MSG_") + QString::number(id.address, 16).toUpper(); +} + +QString DBCFile::newSignalName(const MessageId &id) { + auto m = msg(id); + assert(m != nullptr); + + QString name; + + for (int i = 1; /**/; ++i) { + name = QString("NEW_SIGNAL_%1").arg(i); + if (m->sig(name) == nullptr) break; + } + + return name; +} + +std::map DBCFile::getMessages() { + return msgs; +} + +const cabana::Msg *DBCFile::msg(const MessageId &id) const { + return msg(id.address); +} + +const cabana::Msg *DBCFile::msg(uint32_t address) const { + auto it = msgs.find(address); + return it != msgs.end() ? &it->second : nullptr; +} + +const cabana::Msg* DBCFile::msg(const QString &name) { + for (auto &[_, msg] : msgs) { + if (msg.name == name) { + return &msg; + } + } + + return nullptr; +} + + +QStringList DBCFile::signalNames() const { + // Used for autocompletion + QStringList ret; + for (auto const& [_, msg] : msgs) { + for (auto sig: msg.getSignals()) { + ret << sig->name; + } + } + ret.sort(); + ret.removeDuplicates(); + return ret; +} + +int DBCFile::signalCount(const MessageId &id) const { + if (msgs.count(id.address) == 0) return 0; + return msgs.at(id.address).sigs.size(); +} + +int DBCFile::signalCount() const { + int total = 0; + for (auto const& [_, msg] : msgs) { + total += msg.sigs.size(); + } + return total; +} + +int DBCFile::msgCount() const { + return msgs.size(); +} + +QString DBCFile::name() const { + return name_.isEmpty() ? "untitled" : name_; +} + +bool DBCFile::isEmpty() const { + return (signalCount() == 0) && name_.isEmpty(); +} + +void DBCFile::parseExtraInfo(const QString &content) { static QRegularExpression bo_regexp(R"(^BO_ (\w+) (\w+) *: (\w+) (\w+))"); static QRegularExpression sg_regexp(R"(^SG_ (\w+) : (\d+)\|(\d+)@(\d+)([\+|\-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] \"(.*)\" (.*))"); static QRegularExpression sgm_regexp(R"(^SG_ (\w+) (\w+) *: (\d+)\|(\d+)@(\d+)([\+|\-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] \"(.*)\" (.*))"); @@ -108,7 +285,7 @@ void DBCManager::parseExtraInfo(const QString &content) { } } -QString DBCManager::generateDBC() { +QString DBCFile::generateDBC() { QString dbc_string, signal_comment, val_desc; for (auto &[address, m] : msgs) { dbc_string += QString("BO_ %1 %2: %3 XXX\n").arg(address).arg(m.name).arg(m.size); @@ -139,76 +316,3 @@ QString DBCManager::generateDBC() { } return dbc_string + signal_comment + val_desc; } - -void DBCManager::updateMsg(const MessageId &id, const QString &name, uint32_t size) { - auto &m = msgs[id.address]; - m.name = name; - m.size = size; - - // This DBC applies to all active sources, emit for every source - for (uint8_t source : sources) { - emit msgUpdated({.source = source, .address = id.address}); - } -} - -void DBCManager::removeMsg(const MessageId &id) { - msgs.erase(id.address); - - // This DBC applies to all active sources, emit for every source - for (uint8_t source : sources) { - emit msgRemoved({.source = source, .address = id.address}); - } -} - -void DBCManager::addSignal(const MessageId &id, const cabana::Signal &sig) { - if (auto m = const_cast(msg(id.address))) { - m->sigs.push_back(sig); - auto s = &m->sigs.last(); - - // This DBC applies to all active sources, emit for every source - for (uint8_t source : sources) { - emit signalAdded({.source = source, .address = id.address}, s); - } - } -} - -void DBCManager::updateSignal(const MessageId &id, const QString &sig_name, const cabana::Signal &sig) { - if (auto m = const_cast(msg(id))) { - if (auto s = (cabana::Signal *)m->sig(sig_name)) { - *s = sig; - emit signalUpdated(s); - } - } -} - -void DBCManager::removeSignal(const MessageId &id, const QString &sig_name) { - if (auto m = const_cast(msg(id))) { - auto it = std::find_if(m->sigs.begin(), m->sigs.end(), [&](auto &s) { return s.name == sig_name; }); - if (it != m->sigs.end()) { - emit signalRemoved(&(*it)); - m->sigs.erase(it); - } - } -} - -QStringList DBCManager::signalNames() { - // Used for autocompletion - QStringList ret; - for (auto const& [_, msg] : msgs) { - for (auto sig: msg.getSignals()) { - ret << sig->name; - } - } - ret.sort(); - ret.removeDuplicates(); - return ret; -} - -void DBCManager::updateSources(const QSet &s) { - sources = s; -} - -DBCManager *dbc() { - static DBCManager dbc_manager(nullptr); - return &dbc_manager; -} diff --git a/tools/cabana/dbc/dbcfile.h b/tools/cabana/dbc/dbcfile.h new file mode 100644 index 0000000000..e048dc5839 --- /dev/null +++ b/tools/cabana/dbc/dbcfile.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "tools/cabana/dbc/dbc.h" + +const QString AUTO_SAVE_EXTENSION = ".tmp"; + + +class DBCFile : public QObject { + Q_OBJECT + +public: + DBCFile(const QString &dbc_file_name, QObject *parent=nullptr); + DBCFile(const QString &name, const QString &content, QObject *parent=nullptr); + ~DBCFile() {} + + void open(const QString &content); + + bool save(); + bool saveAs(const QString &new_filename); + bool autoSave(); + bool writeContents(const QString &fn); + void cleanupAutoSaveFile(); + QString generateDBC(); + + cabana::Signal *addSignal(const MessageId &id, const cabana::Signal &sig); + cabana::Signal *updateSignal(const MessageId &id, const QString &sig_name, const cabana::Signal &sig); + cabana::Signal *getSignal(const MessageId &id, const QString &sig_name); + void removeSignal(const MessageId &id, const QString &sig_name); + + void updateMsg(const MessageId &id, const QString &name, uint32_t size); + void removeMsg(const MessageId &id); + + QString newMsgName(const MessageId &id); + QString newSignalName(const MessageId &id); + + std::map getMessages(); + const cabana::Msg *msg(const MessageId &id) const; + const cabana::Msg *msg(uint32_t address) const; + const cabana::Msg* msg(const QString &name); + QStringList signalNames() const; + int signalCount(const MessageId &id) const; + int signalCount() const; + int msgCount() const; + QString name() const; + bool isEmpty() const; + + QString filename; + +private: + void parseExtraInfo(const QString &content); + std::map msgs; + QString name_; +}; diff --git a/tools/cabana/dbc/dbcmanager.cc b/tools/cabana/dbc/dbcmanager.cc new file mode 100644 index 0000000000..390ded8512 --- /dev/null +++ b/tools/cabana/dbc/dbcmanager.cc @@ -0,0 +1,305 @@ +#include "tools/cabana/dbc/dbcmanager.h" + +#include +#include +#include +#include +#include +#include + + +bool DBCManager::open(SourceSet s, const QString &dbc_file_name, QString *error) { + for (int i = 0; i < dbc_files.size(); i++) { + auto [ss, dbc_file] = dbc_files[i]; + + // Check if file is already open, and merge sources + if (dbc_file->filename == dbc_file_name) { + dbc_files[i] = {ss | s, dbc_file}; + + emit DBCFileChanged(); + return true; + } + } + + try { + dbc_files.push_back({s, new DBCFile(dbc_file_name, this)}); + } catch (std::exception &e) { + if (error) *error = e.what(); + return false; + } + + emit DBCFileChanged(); + return true; +} + +bool DBCManager::open(SourceSet s, const QString &name, const QString &content, QString *error) { + try { + dbc_files.push_back({s, new DBCFile(name, content, this)}); + } catch (std::exception &e) { + if (error) *error = e.what(); + return false; + } + + emit DBCFileChanged(); + return true; +} + +void DBCManager::close(SourceSet s) { + // Build new list of dbc files, removing the ones that match the sourceset + QList> new_dbc_files; + for (auto entry : dbc_files) { + if (entry.first == s) { + delete entry.second; + } else { + new_dbc_files.push_back(entry); + } + } + + dbc_files = new_dbc_files; + emit DBCFileChanged(); +} + +void DBCManager::close(DBCFile *dbc_file) { + assert(dbc_file != nullptr); + + // Build new list of dbc files, removing the one that matches dbc_file* + QList> new_dbc_files; + for (auto entry : dbc_files) { + if (entry.second == dbc_file) { + delete entry.second; + } else { + new_dbc_files.push_back(entry); + } + } + + dbc_files = new_dbc_files; + emit DBCFileChanged(); +} + +void DBCManager::closeAll() { + if (dbc_files.isEmpty()) return; + + while (dbc_files.size()) { + DBCFile *dbc_file = dbc_files.back().second; + dbc_files.pop_back(); + delete dbc_file; + } + emit DBCFileChanged(); +} + +void DBCManager::removeSourcesFromFile(DBCFile *dbc_file, SourceSet s) { + assert(dbc_file != nullptr); + + // Build new list of dbc files, for the given dbc_file* remove s from the current sources + QList> new_dbc_files; + for (auto entry : dbc_files) { + if (entry.second == dbc_file) { + SourceSet ss = (entry.first == SOURCE_ALL) ? sources : entry.first; + ss -= s; + if (ss.empty()) { // Close file if no more sources remain + delete dbc_file; + } else { + new_dbc_files.push_back({ss, dbc_file}); + } + } else { + new_dbc_files.push_back(entry); + } + } + + dbc_files = new_dbc_files; + emit DBCFileChanged(); +} + + +void DBCManager::addSignal(const MessageId &id, const cabana::Signal &sig) { + auto sources_dbc_file = findDBCFile(id); + assert(sources_dbc_file); // Create new DBC? + auto [dbc_sources, dbc_file] = *sources_dbc_file; + + cabana::Signal *s = dbc_file->addSignal(id, sig); + + if (s != nullptr) { + for (uint8_t source : dbc_sources) { + emit signalAdded({.source = source, .address = id.address}, s); + } + } +} + +void DBCManager::updateSignal(const MessageId &id, const QString &sig_name, const cabana::Signal &sig) { + auto sources_dbc_file = findDBCFile(id); + assert(sources_dbc_file); // This should be impossible + auto [_, dbc_file] = *sources_dbc_file; + + cabana::Signal *s = dbc_file->updateSignal(id, sig_name, sig); + + if (s != nullptr) { + emit signalUpdated(s); + } +} + +void DBCManager::removeSignal(const MessageId &id, const QString &sig_name) { + auto sources_dbc_file = findDBCFile(id); + assert(sources_dbc_file); // This should be impossible + auto [_, dbc_file] = *sources_dbc_file; + + cabana::Signal *s = dbc_file->getSignal(id, sig_name); + + if (s != nullptr) { + emit signalRemoved(s); + dbc_file->removeSignal(id, sig_name); + } +} + +void DBCManager::updateMsg(const MessageId &id, const QString &name, uint32_t size) { + auto sources_dbc_file = findDBCFile(id); + assert(sources_dbc_file); // This should be impossible + auto [dbc_sources, dbc_file] = *sources_dbc_file; + + dbc_file->updateMsg(id, name, size); + + for (uint8_t source : dbc_sources) { + emit msgUpdated({.source = source, .address = id.address}); + } +} + +void DBCManager::removeMsg(const MessageId &id) { + auto sources_dbc_file = findDBCFile(id); + assert(sources_dbc_file); // This should be impossible + auto [dbc_sources, dbc_file] = *sources_dbc_file; + + dbc_file->removeMsg(id); + + for (uint8_t source : dbc_sources) { + emit msgRemoved({.source = source, .address = id.address}); + } +} + +QString DBCManager::newMsgName(const MessageId &id) { + auto sources_dbc_file = findDBCFile(id); + assert(sources_dbc_file); // This should be impossible + auto [_, dbc_file] = *sources_dbc_file; + return dbc_file->newMsgName(id); +} + +QString DBCManager::newSignalName(const MessageId &id) { + auto sources_dbc_file = findDBCFile(id); + assert(sources_dbc_file); // This should be impossible + auto [_, dbc_file] = *sources_dbc_file; + return dbc_file->newSignalName(id); +} + +std::map DBCManager::getMessages(uint8_t source) { + std::map ret; + + auto sources_dbc_file = findDBCFile({.source = source, .address = 0}); + if (!sources_dbc_file) { + return ret; + } + + auto [_, dbc_file] = *sources_dbc_file; + + for (auto &[address, msg] : dbc_file->getMessages()) { + MessageId id = {.source = source, .address = address}; + ret[id] = msg; + } + return ret; +} + +const cabana::Msg *DBCManager::msg(const MessageId &id) const { + auto sources_dbc_file = findDBCFile(id); + if (!sources_dbc_file) { + return nullptr; + } + auto [_, dbc_file] = *sources_dbc_file; + return dbc_file->msg(id); +} + +const cabana::Msg* DBCManager::msg(uint8_t source, const QString &name) { + auto sources_dbc_file = findDBCFile({.source = source, .address = 0}); + if (!sources_dbc_file) { + return nullptr; + } + auto [_, dbc_file] = *sources_dbc_file; + return dbc_file->msg(name); +} + +QStringList DBCManager::signalNames() const { + QStringList ret; + + for (auto &[_, dbc_file] : dbc_files) { + ret << dbc_file->signalNames(); + } + + ret.sort(); + ret.removeDuplicates(); + return ret; +} + +int DBCManager::signalCount(const MessageId &id) const { + auto sources_dbc_file = findDBCFile(id); + if (!sources_dbc_file) { + return 0; + } + + auto [_, dbc_file] = *sources_dbc_file; + return dbc_file->signalCount(id); +} + +int DBCManager::signalCount() const { + int ret = 0; + + for (auto &[_, dbc_file] : dbc_files) { + ret += dbc_file->signalCount(); + } + + return ret; +} + +int DBCManager::msgCount() const { + int ret = 0; + + for (auto &[_, dbc_file] : dbc_files) { + ret += dbc_file->msgCount(); + } + + return ret; +} + +int DBCManager::dbcCount() const { + return dbc_files.size(); +} + +int DBCManager::nonEmptyDBCCount() const { + int cnt = 0; + for (auto &[_, dbc_file] : dbc_files) { + if (!dbc_file->isEmpty()) { + cnt++; + } + } + return cnt; +} + +void DBCManager::updateSources(const SourceSet &s) { + sources = s; +} + +std::optional> DBCManager::findDBCFile(const uint8_t source) const { + // Find DBC file that matches id.source, fall back to SOURCE_ALL if no specific DBC is found + + for (auto &[source_set, dbc_file] : dbc_files) { + if (source_set.contains(source)) return {{source_set, dbc_file}}; + } + for (auto &[source_set, dbc_file] : dbc_files) { + if (source_set == SOURCE_ALL) return {{sources, dbc_file}}; + } + return {}; +} + +std::optional> DBCManager::findDBCFile(const MessageId &id) const { + return findDBCFile(id.source); +} + +DBCManager *dbc() { + static DBCManager dbc_manager(nullptr); + return &dbc_manager; +} diff --git a/tools/cabana/dbc/dbcmanager.h b/tools/cabana/dbc/dbcmanager.h new file mode 100644 index 0000000000..96d9380dbd --- /dev/null +++ b/tools/cabana/dbc/dbcmanager.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "tools/cabana/dbc/dbc.h" +#include "tools/cabana/dbc/dbcfile.h" + +typedef QSet SourceSet; +const SourceSet SOURCE_ALL = {}; + +class DBCManager : public QObject { + Q_OBJECT + +public: + DBCManager(QObject *parent) {} + ~DBCManager() {} + bool open(SourceSet s, const QString &dbc_file_name, QString *error = nullptr); + bool open(SourceSet s, const QString &name, const QString &content, QString *error = nullptr); + void close(SourceSet s); + void close(DBCFile *dbc_file); + void closeAll(); + void removeSourcesFromFile(DBCFile *dbc_file, SourceSet s); + + void addSignal(const MessageId &id, const cabana::Signal &sig); + void updateSignal(const MessageId &id, const QString &sig_name, const cabana::Signal &sig); + void removeSignal(const MessageId &id, const QString &sig_name); + + void updateMsg(const MessageId &id, const QString &name, uint32_t size); + void removeMsg(const MessageId &id); + + QString newMsgName(const MessageId &id); + QString newSignalName(const MessageId &id); + + std::map getMessages(uint8_t source); + const cabana::Msg *msg(const MessageId &id) const; + const cabana::Msg* msg(uint8_t source, const QString &name); + + QStringList signalNames() const; + int signalCount(const MessageId &id) const; + int signalCount() const; + int msgCount() const; + int dbcCount() const; + int nonEmptyDBCCount() const; + + std::optional> findDBCFile(const uint8_t source) const; + std::optional> findDBCFile(const MessageId &id) const; + + QList> dbc_files; + +private: + SourceSet sources; + +public slots: + void updateSources(const SourceSet &s); + +signals: + void signalAdded(MessageId id, const cabana::Signal *sig); + void signalRemoved(const cabana::Signal *sig); + void signalUpdated(const cabana::Signal *sig); + void msgUpdated(MessageId id); + void msgRemoved(MessageId id); + void DBCFileChanged(); +}; + +DBCManager *dbc(); + +inline QString msgName(const MessageId &id) { + auto msg = dbc()->msg(id); + return msg ? msg->name : UNTITLED; +} + +inline QString toString(SourceSet ss) { + if (ss == SOURCE_ALL) { + return "all"; + } else { + QStringList ret; + QList source_list = ss.values(); + std::sort(source_list.begin(), source_list.end()); + for (auto s : source_list) { + ret << QString::number(s); + } + return ret.join(", "); + } +} diff --git a/tools/cabana/generate_dbc_json.py b/tools/cabana/dbc/generate_dbc_json.py similarity index 100% rename from tools/cabana/generate_dbc_json.py rename to tools/cabana/dbc/generate_dbc_json.py diff --git a/tools/cabana/dbcmanager.h b/tools/cabana/dbcmanager.h deleted file mode 100644 index dd3d9811c7..0000000000 --- a/tools/cabana/dbcmanager.h +++ /dev/null @@ -1,78 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/dbc.h" - -class DBCManager : public QObject { - Q_OBJECT - -public: - DBCManager(QObject *parent) {} - ~DBCManager() {} - bool open(const QString &dbc_file_name, QString *error = nullptr); - bool open(const QString &name, const QString &content, QString *error = nullptr); - QString generateDBC(); - void addSignal(const MessageId &id, const cabana::Signal &sig); - void updateSignal(const MessageId &id, const QString &sig_name, const cabana::Signal &sig); - void removeSignal(const MessageId &id, const QString &sig_name); - inline int msgCount() const { return msgs.size(); } - - inline QString name() const { return name_; } - void updateMsg(const MessageId &id, const QString &name, uint32_t size); - void removeMsg(const MessageId &id); - inline std::map getMessages(uint8_t source) { - std::map ret; - for (auto &[address, msg] : msgs) { - MessageId id = {.source = source, .address = address}; - ret[id] = msg; - } - return ret; - } - inline const cabana::Msg *msg(const MessageId &id) const { return msg(id.address); } - inline const cabana::Msg* msg(uint8_t source, const QString &name) { - for (auto &[_, msg] : msgs) { - if (msg.name == name) { - return &msg; - } - } - - return nullptr; - } - QStringList signalNames(); - -public slots: - void updateSources(const QSet &s); - -signals: - void signalAdded(MessageId id, const cabana::Signal *sig); - void signalRemoved(const cabana::Signal *sig); - void signalUpdated(const cabana::Signal *sig); - void msgUpdated(MessageId id); - void msgRemoved(MessageId id); - void DBCFileChanged(); - -private: - void parseExtraInfo(const QString &content); - std::map msgs; - QString name_; - QSet sources; - - inline const cabana::Msg *msg(uint32_t address) const { - auto it = msgs.find(address); - return it != msgs.end() ? &it->second : nullptr; - } -}; - -DBCManager *dbc(); - -inline QString msgName(const MessageId &id) { - auto msg = dbc()->msg(id); - return msg ? msg->name : UNTITLED; -} diff --git a/tools/cabana/detailwidget.cc b/tools/cabana/detailwidget.cc index ab723fa0e2..dd12d4c8a3 100644 --- a/tools/cabana/detailwidget.cc +++ b/tools/cabana/detailwidget.cc @@ -22,7 +22,7 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart // message title QHBoxLayout *title_layout = new QHBoxLayout(); - title_layout->setContentsMargins(0, 6, 0, 0); + title_layout->setContentsMargins(3, 6, 3, 0); time_label = new QLabel(this); time_label->setToolTip(tr("Current time")); time_label->setStyleSheet("QLabel{font-weight:bold;}"); @@ -32,9 +32,9 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart name_label->setAlignment(Qt::AlignCenter); name_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); title_layout->addWidget(name_label); - auto edit_btn = toolButton("pencil", tr("Edit Message")); + auto edit_btn = new ToolButton("pencil", tr("Edit Message")); title_layout->addWidget(edit_btn); - remove_btn = toolButton("x-lg", tr("Remove Message")); + remove_btn = new ToolButton("x-lg", tr("Remove Message")); title_layout->addWidget(remove_btn); main_layout->addLayout(title_layout); @@ -48,7 +48,6 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart // msg widget splitter = new QSplitter(Qt::Vertical, this); - splitter->setAutoFillBackground(true); splitter->addWidget(binary_view = new BinaryView(this)); splitter->addWidget(signal_view = new SignalView(charts, this)); binary_view->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); diff --git a/tools/cabana/detailwidget.h b/tools/cabana/detailwidget.h index cc5bffd534..074ce3b4d3 100644 --- a/tools/cabana/detailwidget.h +++ b/tools/cabana/detailwidget.h @@ -6,9 +6,9 @@ #include "selfdrive/ui/qt/widgets/controls.h" #include "tools/cabana/binaryview.h" -#include "tools/cabana/chartswidget.h" +#include "tools/cabana/chart/chartswidget.h" #include "tools/cabana/historylog.h" -#include "tools/cabana/signaledit.h" +#include "tools/cabana/signalview.h" class EditMessageDialog : public QDialog { public: @@ -30,7 +30,6 @@ public: DetailWidget(ChartsWidget *charts, QWidget *parent); void setMessage(const MessageId &message_id); void refresh(); - QSize minimumSizeHint() const override { return binary_view->minimumSizeHint(); } private: void showTabBarContextMenu(const QPoint &pt); diff --git a/tools/cabana/historylog.cc b/tools/cabana/historylog.cc index 3e80a6a697..4c193666dc 100644 --- a/tools/cabana/historylog.cc +++ b/tools/cabana/historylog.cc @@ -21,7 +21,7 @@ QVariant HistoryLogModel::data(const QModelIndex &index, int role) const { } else if (role == BytesRole) { return m.data; } else if (role == Qt::TextAlignmentRole) { - return Qt::AlignRight; + return (uint32_t)(Qt::AlignRight | Qt::AlignVCenter); } return {}; } @@ -39,7 +39,7 @@ void HistoryLogModel::refresh(bool fetch_message) { last_fetch_time = 0; has_more_data = true; messages.clear(); - hex_colors.clear(); + hex_colors = {}; if (fetch_message) { updateState(); } @@ -119,14 +119,15 @@ template std::deque HistoryLogModel::fetchData(InputIt first, InputIt last, uint64_t min_time) { std::deque msgs; QVector values(sigs.size()); - for (; first != last && first->mono_time > min_time; ++first) { + for (; first != last && (*first)->mono_time > min_time; ++first) { + const CanEvent *e = *first; for (int i = 0; i < sigs.size(); ++i) { - values[i] = get_raw_value(first->dat, first->size, *sigs[i]); + values[i] = get_raw_value(e->dat, e->size, *sigs[i]); } if (!filter_cmp || filter_cmp(values[filter_sig_idx], filter_value)) { auto &m = msgs.emplace_back(); - m.mono_time = first->mono_time; - m.data = QByteArray((const char *)first->dat, first->size); + m.mono_time = e->mono_time; + m.data = QByteArray((const char *)e->dat, e->size); m.sig_values = values; if (msgs.size() >= batch_size && min_time == 0) { return msgs; @@ -137,27 +138,32 @@ std::deque HistoryLogModel::fetchData(InputIt first, I } std::deque HistoryLogModel::fetchData(uint64_t from_time, uint64_t min_time) { - const auto &events = can->events().at(msg_id); + 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 auto speed = can->getSpeed(); if (dynamic_mode) { - auto first = std::upper_bound(events.rbegin(), events.rend(), CanEvent{.mono_time=from_time}, std::greater()); + auto first = std::upper_bound(events.rbegin(), events.rend(), from_time, [](uint64_t ts, auto e) { + return ts > e->mono_time; + }); 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(it->data, it->mono_time / (double)1e9, freq); + hex_colors.compute(it->data.data(), it->data.size(), it->mono_time / (double)1e9, speed, freq); it->colors = hex_colors.colors; } } return msgs; } else { assert(min_time == 0); - auto first = std::upper_bound(events.begin(), events.end(), CanEvent{.mono_time=from_time}); - auto msgs = fetchData(first, events.end(), 0); + auto first = std::upper_bound(events.cbegin(), events.cend(), from_time, [](uint64_t ts, auto e) { + return ts < e->mono_time; + }); + auto msgs = fetchData(first, events.cend(), 0); if (update_colors) { for (auto it = msgs.begin(); it != msgs.end(); ++it) { - hex_colors.compute(it->data, it->mono_time / (double)1e9, freq); + hex_colors.compute(it->data.data(), it->data.size(), it->mono_time / (double)1e9, speed, freq); it->colors = hex_colors.colors; } } @@ -186,6 +192,7 @@ void HeaderView::paintSection(QPainter *painter, const QRect &rect, int logicalI painter->fillRect(rect, bg_role.value()); } QString text = model()->headerData(logicalIndex, Qt::Horizontal, Qt::DisplayRole).toString(); + painter->setPen(palette().color(settings.theme == DARK_THEME ? QPalette::BrightText : QPalette::Text)); painter->drawText(rect.adjusted(5, 3, -5, -3), defaultAlignment(), text.replace(QChar('_'), ' ')); } diff --git a/tools/cabana/historylog.h b/tools/cabana/historylog.h index 119b61a0e8..1f8c157f21 100644 --- a/tools/cabana/historylog.h +++ b/tools/cabana/historylog.h @@ -7,7 +7,7 @@ #include #include -#include "tools/cabana/dbcmanager.h" +#include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" #include "tools/cabana/util.h" @@ -54,7 +54,7 @@ public: std::deque fetchData(uint64_t from_time, uint64_t min_time = 0); MessageId msg_id; - ChangeTracker hex_colors; + CanData hex_colors; bool has_more_data = true; const int batch_size = 50; int filter_sig_idx = -1; diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index befbf270c6..891bfc4bb1 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -17,7 +17,8 @@ #include #include "tools/cabana/commands.h" -#include "tools/cabana/route.h" +#include "tools/cabana/streamselector.h" +#include "tools/cabana/streams/replaystream.h" static MainWindow *main_win = nullptr; void qLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { @@ -39,10 +40,9 @@ MainWindow::MainWindow() : QMainWindow() { setGeometry(QApplication::desktop()->availableGeometry(this)); } restoreState(settings.window_state); - messages_widget->restoreHeaderState(settings.message_header_state); qRegisterMetaType("uint64_t"); - qRegisterMetaType>("QSet"); + qRegisterMetaType("SourceSet"); qRegisterMetaType("ReplyMsgType"); installMessageHandler([this](ReplyMsgType type, const std::string msg) { // use queued connection to recv the log messages from replay. @@ -54,9 +54,13 @@ MainWindow::MainWindow() : QMainWindow() { main_win = this; qInstallMessageHandler(qLogMessageHandler); - QFile json_file("./car_fingerprint_to_dbc.json"); - if (json_file.open(QIODevice::ReadOnly)) { - fingerprint_to_dbc = QJsonDocument::fromJson(json_file.readAll()); + + for (const QString &fn : {"./dbc/car_fingerprint_to_dbc.json", "./tools/cabana/dbc/car_fingerprint_to_dbc.json"}) { + QFile json_file(fn); + if (json_file.open(QIODevice::ReadOnly)) { + fingerprint_to_dbc = QJsonDocument::fromJson(json_file.readAll()); + break; + } } setStyleSheet(QString(R"(QMainWindow::separator { @@ -72,6 +76,7 @@ MainWindow::MainWindow() : QMainWindow() { QObject::connect(can, &AbstractStream::eventsMerged, this, &MainWindow::updateStatus); QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &MainWindow::DBCFileChanged); QObject::connect(can, &AbstractStream::sourcesUpdated, dbc(), &DBCManager::updateSources); + QObject::connect(can, &AbstractStream::sourcesUpdated, this, &MainWindow::updateSources); QObject::connect(UndoStack::instance(), &QUndoStack::cleanChanged, this, &MainWindow::undoStackCleanChanged); QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &MainWindow::undoStackIndexChanged); QObject::connect(&settings, &Settings::changed, this, &MainWindow::updateStatus); @@ -87,6 +92,8 @@ void MainWindow::createActions() { file_menu->addAction(tr("New DBC File"), this, &MainWindow::newFile)->setShortcuts(QKeySequence::New); file_menu->addAction(tr("Open DBC File..."), this, &MainWindow::openFile)->setShortcuts(QKeySequence::Open); + manage_dbcs_menu = file_menu->addMenu(tr("Manage &DBC Files")); + open_recent_menu = file_menu->addMenu(tr("Open &Recent")); for (int i = 0; i < MAX_RECENT_FILES; ++i) { recent_files_acts[i] = new QAction(this); @@ -105,12 +112,17 @@ void MainWindow::createActions() { load_opendbc_menu->addAction(QString::fromStdString(name), this, &MainWindow::openOpendbcFile); } - file_menu->addAction(tr("Load DBC From Clipboard"), this, &MainWindow::loadDBCFromClipboard); + file_menu->addAction(tr("Load DBC From Clipboard"), [=]() { loadFromClipboard(); }); file_menu->addSeparator(); - file_menu->addAction(tr("Save DBC..."), this, &MainWindow::save)->setShortcuts(QKeySequence::Save); - file_menu->addAction(tr("Save DBC As..."), this, &MainWindow::saveAs)->setShortcuts(QKeySequence::SaveAs); - file_menu->addAction(tr("Copy DBC To Clipboard"), this, &MainWindow::saveDBCToClipboard); + 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); + + 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); @@ -217,6 +229,7 @@ void MainWindow::undoStackIndexChanged(int index) { prev_undostack_index = index; prev_undostack_count = count; autoSave(); + updateLoadSaveMenus(); } void MainWindow::undoStackCleanChanged(bool clean) { @@ -229,23 +242,26 @@ void MainWindow::undoStackCleanChanged(bool clean) { void MainWindow::DBCFileChanged() { UndoStack::instance()->clear(); - setWindowFilePath(QString("%1").arg(dbc()->name())); + updateLoadSaveMenus(); } void MainWindow::openRoute() { - OpenRouteDialog dlg(this); + StreamSelector dlg(this); + dlg.addStreamWidget(ReplayStream::widget(&can)); if (dlg.exec()) { center_widget->clear(); charts_widget->removeAll(); statusBar()->showMessage(tr("Route %1 loaded").arg(can->routeName()), 2000); - } else if (dlg.failedToLoad()) { + } else if (dlg.failed()) { close(); } } void MainWindow::newFile() { remindSaveChanges(); - dbc()->open("untitled.dbc", ""); + dbc()->closeAll(); + dbc()->open(SOURCE_ALL, "", ""); + updateLoadSaveMenus(); } void MainWindow::openFile() { @@ -256,7 +272,21 @@ void MainWindow::openFile() { } } -void MainWindow::loadFile(const QString &fn) { +void MainWindow::openFileForSource(SourceSet s) { + QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), settings.last_dir, "DBC (*.dbc)"); + if (!fn.isEmpty()) { + loadFile(fn, s, false); + } +} + +void MainWindow::newFileForSource(SourceSet s) { + remindSaveChanges(); + + dbc()->close(s); + dbc()->open(s, "", ""); +} + +void MainWindow::loadFile(const QString &fn, SourceSet s, bool close_all) { if (!fn.isEmpty()) { QString dbc_fn = fn; @@ -269,20 +299,25 @@ void MainWindow::loadFile(const QString &fn) { } } - QFile file(dbc_fn); - if (file.open(QIODevice::ReadOnly)) { - auto dbc_name = QFileInfo(fn).baseName(); - QString error; - bool ret = dbc()->open(dbc_name, file.readAll(), &error); - if (ret) { - setCurrentFile(fn); - statusBar()->showMessage(tr("DBC File %1 loaded").arg(fn), 2000); - } else { - QMessageBox msg_box(QMessageBox::Warning, tr("Failed to load DBC file"), tr("Failed to parse DBC file %1").arg(fn)); - msg_box.setDetailedText(error); - msg_box.exec(); - } + auto dbc_name = QFileInfo(fn).baseName(); + QString error; + + if (close_all) { + dbc()->closeAll(); } + + dbc()->close(s); + bool ret = dbc()->open(s, dbc_fn, &error); + if (ret) { + updateRecentFiles(fn); + statusBar()->showMessage(tr("DBC File %1 loaded").arg(fn), 2000); + } else { + QMessageBox msg_box(QMessageBox::Warning, tr("Failed to load DBC file"), tr("Failed to parse DBC file %1").arg(fn)); + msg_box.setDetailedText(error); + msg_box.exec(); + } + + updateLoadSaveMenus(); } } @@ -301,17 +336,27 @@ void MainWindow::openRecentFile() { } void MainWindow::loadDBCFromOpendbc(const QString &name) { - if (name != dbc()->name()) { - remindSaveChanges(); - dbc()->open(name); - } + remindSaveChanges(); + + QString opendbc_file_path = QString("%1/%2.dbc").arg(OPENDBC_FILE_PATH, name); + + dbc()->closeAll(); + dbc()->open(SOURCE_ALL, opendbc_file_path); + + updateLoadSaveMenus(); } -void MainWindow::loadDBCFromClipboard() { +void MainWindow::loadFromClipboard(SourceSet s, bool close_all) { remindSaveChanges(); QString dbc_str = QGuiApplication::clipboard()->text(); QString error; - bool ret = dbc()->open("clipboard", dbc_str, &error); + + if (close_all) { + dbc()->closeAll(); + } + + dbc()->close(s); + bool ret = dbc()->open(s, "", dbc_str, &error); if (ret && dbc()->msgCount() > 0) { QMessageBox::information(this, tr("Load From Clipboard"), tr("DBC Successfully Loaded!")); } else { @@ -325,7 +370,7 @@ void MainWindow::loadDBCFromClipboard() { void MainWindow::loadDBCFromFingerprint() { // Don't overwrite already loaded DBC - if (!dbc()->name().isEmpty()) { + if (dbc()->msgCount()) { return; } @@ -334,7 +379,7 @@ void MainWindow::loadDBCFromFingerprint() { if (can->liveStreaming()) { video_dock->setWindowTitle(can->routeName()); } else { - video_dock->setWindowTitle(tr("ROUTE: %1 FINGERPINT: %2").arg(can->routeName()).arg(fingerprint.isEmpty() ? tr("Unknown Car") : fingerprint)); + video_dock->setWindowTitle(tr("ROUTE: %1 FINGERPRINT: %2").arg(can->routeName()).arg(fingerprint.isEmpty() ? tr("Unknown Car") : fingerprint)); } if (!fingerprint.isEmpty()) { auto dbc_name = fingerprint_to_dbc[fingerprint]; @@ -347,54 +392,233 @@ void MainWindow::loadDBCFromFingerprint() { } void MainWindow::save() { - if (current_file.isEmpty()) { - saveAs(); - } else { - saveFile(current_file); + // Save all open DBC files + for (auto &[s, dbc_file] : dbc()->dbc_files) { + if (dbc_file->isEmpty()) continue; + saveFile(dbc_file); + } +} + +void MainWindow::saveAs() { + // Save as all open DBC files. Should not be called with more than 1 file open + for (auto &[s, dbc_file] : dbc()->dbc_files) { + if (dbc_file->isEmpty()) continue; + saveFileAs(dbc_file); } } + void MainWindow::autoSave() { - if (!current_file.isEmpty() && !UndoStack::instance()->isClean()) { - QFile file(current_file + AUTO_SAVE_EXTENSION); - if (file.open(QIODevice::WriteOnly)) { - file.write(dbc()->generateDBC().toUtf8()); + if (!UndoStack::instance()->isClean()) { + for (auto &[_, dbc_file] : dbc()->dbc_files) { + if (!dbc_file->filename.isEmpty()) { + dbc_file->autoSave(); + } } } } void MainWindow::cleanupAutoSaveFile() { - if (!current_file.isEmpty()) { - QFile::remove(current_file + AUTO_SAVE_EXTENSION); + for (auto &[_, dbc_file] : dbc()->dbc_files) { + dbc_file->cleanupAutoSaveFile(); } } -void MainWindow::saveFile(const QString &fn) { - cleanupAutoSaveFile(); - QFile file(fn); - if (file.open(QIODevice::WriteOnly)) { - file.write(dbc()->generateDBC().toUtf8()); - UndoStack::instance()->setClean(); - setCurrentFile(fn); - statusBar()->showMessage(tr("File saved"), 2000); +void MainWindow::closeFile(DBCFile *dbc_file) { + assert(dbc_file != nullptr); + remindSaveChanges(); + + dbc()->close(dbc_file); + + // Ensure we always have at least one file open + if (dbc()->dbcCount() == 0) { + newFile(); } } -void MainWindow::saveAs() { - QString fn = QFileDialog::getSaveFileName(this, tr("Save File"), QDir::cleanPath(settings.last_dir + "/untitled.dbc"), tr("DBC (*.dbc)")); +void MainWindow::saveFile(DBCFile *dbc_file) { + assert(dbc_file != nullptr); + + SourceSet s; + for (auto &[s_, dbc_file_] : dbc()->dbc_files) { + if (dbc_file_ == dbc_file) { + s = s_; + break; + } + } + + if (!dbc_file->filename.isEmpty()) { + dbc_file->save(); + updateRecentFiles(dbc_file->filename); + } else if (!dbc_file->isEmpty()) { + QString fn = QFileDialog::getSaveFileName(this, tr("Save File (bus: %1)").arg(toString(s)), QDir::cleanPath(settings.last_dir + "/untitled.dbc"), tr("DBC (*.dbc)")); + if (!fn.isEmpty()) { + dbc_file->saveAs(fn); + updateRecentFiles(fn); + } + } + + UndoStack::instance()->setClean(); + statusBar()->showMessage(tr("File saved"), 2000); + updateLoadSaveMenus(); +} + +void MainWindow::saveFileAs(DBCFile *dbc_file) { + assert(dbc_file != nullptr); + + SourceSet s; + for (auto &[s_, dbc_file_] : dbc()->dbc_files) { + if (dbc_file_ == dbc_file) { + s = s_; + break; + } + } + + QString fn = QFileDialog::getSaveFileName(this, tr("Save File (bus: %1)").arg(toString(s)), QDir::cleanPath(settings.last_dir + "/untitled.dbc"), tr("DBC (*.dbc)")); if (!fn.isEmpty()) { - saveFile(fn); + dbc_file->saveAs(fn); + updateRecentFiles(fn); + updateLoadSaveMenus(); } } -void MainWindow::saveDBCToClipboard() { - QGuiApplication::clipboard()->setText(dbc()->generateDBC()); +void MainWindow::removeBusFromFile(DBCFile *dbc_file, uint8_t source) { + assert(dbc_file != nullptr); + SourceSet ss = {source, uint8_t(source + 128), uint8_t(source + 192)}; + dbc()->removeSourcesFromFile(dbc_file, ss); + updateLoadSaveMenus(); +} + + +void MainWindow::saveToClipboard() { + // Copy all open DBC files to clipboard. Should not be called with more than 1 file open + for (auto &[s, dbc_file] : dbc()->dbc_files) { + if (dbc_file->isEmpty()) continue; + saveFileToClipboard(dbc_file); + } +} + +void MainWindow::saveFileToClipboard(DBCFile *dbc_file) { + assert(dbc_file != nullptr); + QGuiApplication::clipboard()->setText(dbc_file->generateDBC()); QMessageBox::information(this, tr("Copy To Clipboard"), tr("DBC Successfully copied!")); } -void MainWindow::setCurrentFile(const QString &fn) { - current_file = fn; - setWindowFilePath(QString("%1").arg(fn)); +void MainWindow::updateSources(const SourceSet &s) { + sources = s; + updateLoadSaveMenus(); +} + +void MainWindow::updateLoadSaveMenus() { + int cnt = dbc()->nonEmptyDBCCount(); + save_dbc->setEnabled(cnt > 0); + + if (cnt > 1) { + save_dbc->setText(tr("Save %1 DBCs...").arg(dbc()->dbcCount())); + } else { + save_dbc->setText(tr("Save DBC...")); + } + + save_dbc_as->setEnabled(cnt == 1); + + // TODO: Support clipboard for multiple files + copy_dbc_to_clipboard->setEnabled(cnt == 1); + + + QList sources_sorted = sources.toList(); + std::sort(sources_sorted.begin(), sources_sorted.end()); + + manage_dbcs_menu->clear(); + + for (uint8_t source : sources_sorted) { + if (source >= 64) continue; // Sent and blocked buses are handled implicitly + + SourceSet ss = {source, uint8_t(source + 128), uint8_t(source + 192)}; + + QMenu *bus_menu = new QMenu(this); + + // New + QAction *new_action = new QAction(this); + new_action->setText(tr("New DBC File...")); + QObject::connect(new_action, &QAction::triggered, [=]() { newFileForSource(ss); }); + bus_menu->addAction(new_action); + + // Open + QAction *open_action = new QAction(this); + open_action->setText(tr("Open DBC File...")); + QObject::connect(open_action, &QAction::triggered, [=]() { openFileForSource(ss); }); + bus_menu->addAction(open_action); + + // Open + QAction *load_clipboard_action = new QAction(this); + load_clipboard_action->setText(tr("Load DBC From Clipboard...")); + QObject::connect(load_clipboard_action, &QAction::triggered, [=]() { loadFromClipboard(ss, false); }); + bus_menu->addAction(load_clipboard_action); + + // Show sub-menu for each dbc for this source. + QStringList bus_menu_fns; + for (auto it : dbc()->dbc_files) { + auto &[src, dbc_file] = it; + if (!src.contains(source) && (src != SOURCE_ALL)) { + continue; + } + + QString fn = dbc_file->filename.isEmpty() ? "untitled" : QFileInfo(dbc_file->filename).baseName(); + // QMenu *manage_menu = bus_menu; + + bus_menu->addSeparator(); + QAction *fn_action = new QAction(this); + fn_action->setText(fn + " (" + toString(src) + ")"); + fn_action->setEnabled(false); + bus_menu->addAction(fn_action); + + // Save + QAction *save_action = new QAction(this); + save_action->setText(tr("Save...")); + bus_menu->addAction(save_action); + QObject::connect(save_action, &QAction::triggered, [=](){ saveFile(it.second); }); + + // Save as + QAction *save_as_action = new QAction(this); + save_as_action->setText(tr("Save As...")); + bus_menu->addAction(save_as_action); + QObject::connect(save_as_action, &QAction::triggered, [=](){ saveFileAs(it.second); }); + + // Copy to clipboard + QAction *save_clipboard_action = new QAction(this); + save_clipboard_action->setText(tr("Copy to Clipboard...")); + bus_menu->addAction(save_clipboard_action); + QObject::connect(save_clipboard_action, &QAction::triggered, [=](){ saveFileToClipboard(it.second); }); + + // Remove from this bus + QAction *remove_action = new QAction(this); + remove_action->setText(tr("Remove from this bus...")); + bus_menu->addAction(remove_action); + QObject::connect(remove_action, &QAction::triggered, [=](){ removeBusFromFile(it.second, source); }); + + // Close/Remove from all buses + QAction *close_action = new QAction(this); + close_action->setText(tr("Remove from all buses...")); + bus_menu->addAction(close_action); + QObject::connect(close_action, &QAction::triggered, [=](){ closeFile(it.second); }); + + bus_menu_fns << fn; + } + + manage_dbcs_menu->addMenu(bus_menu); + QString bus_menu_title = bus_menu_fns.size() ? bus_menu_fns.join(", ") : "No DBCs loaded"; + bus_menu->setTitle(tr("Bus %1 (%2)").arg(source).arg(bus_menu_title)); + } + + QStringList title; + for (auto &[src, dbc_file] : dbc()->dbc_files) { + QString fn = dbc_file->filename.isEmpty() ? "untitled" : QFileInfo(dbc_file->filename).baseName(); + title.push_back(tr("(%1) %2").arg(toString(src)).arg(fn)); + } + setWindowFilePath(title.join(" | ")); +} + +void MainWindow::updateRecentFiles(const QString &fn) { settings.recent_files.removeAll(fn); settings.recent_files.prepend(fn); while (settings.recent_files.size() > MAX_RECENT_FILES) { @@ -432,7 +656,6 @@ void MainWindow::remindSaveChanges() { } } UndoStack::instance()->clear(); - current_file = ""; } void MainWindow::updateDownloadProgress(uint64_t cur, uint64_t total, bool success) { diff --git a/tools/cabana/mainwin.h b/tools/cabana/mainwin.h index 3258b95cde..5e76d86e5c 100644 --- a/tools/cabana/mainwin.h +++ b/tools/cabana/mainwin.h @@ -7,14 +7,13 @@ #include #include -#include "tools/cabana/chartswidget.h" +#include "tools/cabana/chart/chartswidget.h" +#include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/detailwidget.h" #include "tools/cabana/messageswidget.h" #include "tools/cabana/videowidget.h" #include "tools/cabana/tools/findsimilarbits.h" -const QString AUTO_SAVE_EXTENSION = ".tmp"; - class MainWindow : public QMainWindow { Q_OBJECT @@ -22,7 +21,7 @@ public: MainWindow(); void dockCharts(bool dock); void showStatusMessage(const QString &msg, int timeout = 0) { statusBar()->showMessage(msg, timeout); } - void loadFile(const QString &fn); + void loadFile(const QString &fn, SourceSet s = SOURCE_ALL, bool close_all = true); public slots: void openRoute(); @@ -32,10 +31,10 @@ public slots: void openOpendbcFile(); void loadDBCFromOpendbc(const QString &name); void loadDBCFromFingerprint(); - void loadDBCFromClipboard(); void save(); void saveAs(); - void saveDBCToClipboard(); + void saveToClipboard(); + void updateSources(const SourceSet &s); signals: void showMessage(const QString &msg, int timeout); @@ -43,10 +42,17 @@ signals: protected: void remindSaveChanges(); - void saveFile(const QString &fn); + void closeFile(DBCFile *dbc_file); + void saveFile(DBCFile *dbc_file); + void saveFileAs(DBCFile *dbc_file); + void saveFileToClipboard(DBCFile *dbc_file); + void removeBusFromFile(DBCFile *dbc_file, uint8_t source); + void loadFromClipboard(SourceSet s = SOURCE_ALL, bool close_all = true); + void openFileForSource(SourceSet s); + void newFileForSource(SourceSet s); void autoSave(); void cleanupAutoSaveFile(); - void setCurrentFile(const QString &fn); + void updateRecentFiles(const QString &fn); void updateRecentFileActions(); void createActions(); void createDockWindows(); @@ -62,6 +68,7 @@ protected: void onlineHelp(); void toggleFullScreen(); void updateStatus(); + void updateLoadSaveMenus(); VideoWidget *video_widget = nullptr; QDockWidget *video_dock; @@ -74,12 +81,16 @@ protected: QLabel *status_label; QJsonDocument fingerprint_to_dbc; QSplitter *video_splitter;; - QString current_file = ""; enum { MAX_RECENT_FILES = 15 }; QAction *recent_files_acts[MAX_RECENT_FILES] = {}; QMenu *open_recent_menu = nullptr; + QMenu *manage_dbcs_menu = nullptr; + QAction *save_dbc = nullptr; + QAction *save_dbc_as = nullptr; + QAction *copy_dbc_to_clipboard = nullptr; int prev_undostack_index = 0; int prev_undostack_count = 0; + SourceSet sources; friend class OnlineHelp; }; diff --git a/tools/cabana/messageswidget.cc b/tools/cabana/messageswidget.cc index 8dd0bac820..e74b852617 100644 --- a/tools/cabana/messageswidget.cc +++ b/tools/cabana/messageswidget.cc @@ -1,6 +1,7 @@ #include "tools/cabana/messageswidget.h" #include +#include #include #include @@ -9,29 +10,36 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { main_layout->setContentsMargins(0 ,0, 0, 0); // message filter - filter = new QLineEdit(this); + QHBoxLayout *title_layout = new QHBoxLayout(); + title_layout->addWidget(filter = new QLineEdit(this)); QRegularExpression re("\\S+"); filter->setValidator(new QRegularExpressionValidator(re, this)); filter->setClearButtonEnabled(true); filter->setPlaceholderText(tr("filter messages")); - main_layout->addWidget(filter); + title_layout->addWidget(multiple_lines_bytes = new QCheckBox(tr("Multiple Lines Bytes"), this)); + multiple_lines_bytes->setToolTip(tr("Display bytes in multiple lines")); + multiple_lines_bytes->setChecked(settings.multiple_lines_bytes); + main_layout->addLayout(title_layout); // message table - table_widget = new QTableView(this); + view = new MessageView(this); model = new MessageListModel(this); - table_widget->setModel(model); - table_widget->setItemDelegateForColumn(5, new MessageBytesDelegate(table_widget)); - table_widget->setSelectionBehavior(QAbstractItemView::SelectRows); - table_widget->setSelectionMode(QAbstractItemView::SingleSelection); - table_widget->setSortingEnabled(true); - table_widget->sortByColumn(0, Qt::AscendingOrder); - table_widget->setColumnWidth(0, 150); - table_widget->setColumnWidth(1, 50); - table_widget->setColumnWidth(2, 50); - table_widget->setColumnWidth(3, 50); - table_widget->horizontalHeader()->setStretchLastSection(true); - table_widget->verticalHeader()->hide(); - main_layout->addWidget(table_widget); + auto delegate = new MessageBytesDelegate(view, settings.multiple_lines_bytes); + view->setItemDelegate(delegate); + view->setModel(model); + view->setSortingEnabled(true); + view->sortByColumn(0, Qt::AscendingOrder); + view->setAllColumnsShowFocus(true); + view->setEditTriggers(QAbstractItemView::NoEditTriggers); + view->setItemsExpandable(false); + view->setIndentation(0); + view->setRootIsDecorated(false); + // Must be called before setting any header parameters to avoid overriding + restoreHeaderState(settings.message_header_state); + + view->header()->setSectionsMovable(true); + + main_layout->addWidget(view); // suppress QHBoxLayout *suppress_layout = new QHBoxLayout(); @@ -43,6 +51,12 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { // signals/slots QObject::connect(filter, &QLineEdit::textEdited, model, &MessageListModel::setFilterString); + QObject::connect(multiple_lines_bytes, &QCheckBox::stateChanged, [=](int state) { + settings.multiple_lines_bytes = (state == Qt::Checked); + delegate->setMultipleLines(settings.multiple_lines_bytes); + view->setUniformRowHeights(!settings.multiple_lines_bytes); + model->sortMessages(); + }); QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived); QObject::connect(can, &AbstractStream::streamStarted, this, &MessagesWidget::reset); QObject::connect(dbc(), &DBCManager::DBCFileChanged, model, &MessageListModel::sortMessages); @@ -52,8 +66,9 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { if (current_msg_id) { selectMessage(*current_msg_id); } + view->updateBytesSectionSize(); }); - QObject::connect(table_widget->selectionModel(), &QItemSelectionModel::currentChanged, [=](const QModelIndex ¤t, const QModelIndex &previous) { + 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_msg_id || id != *current_msg_id) { @@ -85,7 +100,7 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { void MessagesWidget::selectMessage(const MessageId &msg_id) { if (int row = model->msgs.indexOf(msg_id); row != -1) { - table_widget->selectionModel()->setCurrentIndex(model->index(row, 0), QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect); + view->selectionModel()->setCurrentIndex(model->index(row, 0), QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect); } } @@ -101,7 +116,7 @@ void MessagesWidget::updateSuppressedButtons() { void MessagesWidget::reset() { current_msg_id = std::nullopt; - table_widget->selectionModel()->clear(); + view->selectionModel()->clear(); model->reset(); filter->clear(); updateSuppressedButtons(); @@ -111,8 +126,10 @@ void MessagesWidget::reset() { // MessageListModel QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, int role) const { - if (orientation == Qt::Horizontal && role == Qt::DisplayRole) - return (QString[]){"Name", "Bus", "ID", "Freq", "Count", "Bytes"}[section]; + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { + static const QString titles[] = {"Name", "Bus", "ID", "Freq", "Count", "Bytes"}; + return titles[section]; + } return {}; } @@ -120,12 +137,20 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const { const auto &id = msgs[index.row()]; auto &can_data = can->lastMessage(id); + auto getFreq = [](const CanData &d) -> QString { + if (d.freq > 0 && (can->currentSec() - d.ts - 1.0 / settings.fps) < (5.0 / d.freq)) { + return d.freq >= 1 ? QString::number(std::nearbyint(d.freq)) : QString::number(d.freq, 'f', 2); + } else { + return "--"; + } + }; + if (role == Qt::DisplayRole) { switch (index.column()) { case 0: return msgName(id); case 1: return id.source; - case 2: return QString::number(id.address, 16);; - case 3: return can_data.freq; + case 2: return QString::number(id.address, 16); + case 3: return getFreq(can_data); case 4: return can_data.count; case 5: return toHex(can_data.dat); } @@ -139,7 +164,7 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const { } } return QVariant::fromValue(colors); - } else if (role == BytesRole) { + } else if (role == BytesRole && index.column() == 5) { return can_data.dat; } return {}; @@ -254,3 +279,43 @@ void MessageListModel::reset() { clearSuppress(); endResetModel(); } + +// MessageView + +void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { + QTreeView::drawRow(painter, option, index); + const int gridHint = style()->styleHint(QStyle::SH_Table_GridLineColor, &option, this); + const QColor gridColor = QColor::fromRgba(static_cast(gridHint)); + QPen old_pen = painter->pen(); + painter->setPen(gridColor); + painter->drawLine(option.rect.left(), option.rect.bottom(), option.rect.right(), option.rect.bottom()); + + auto y = option.rect.y(); + painter->translate(visualRect(model()->index(0, 0)).x() - indentation() - .5, -.5); + for (int i = 0; i < header()->count(); ++i) { + painter->translate(header()->sectionSize(header()->logicalIndex(i)), 0); + painter->drawLine(0, y, 0, y + option.rect.height()); + } + painter->setPen(old_pen); + painter->resetTransform(); +} + +void MessageView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { + // Bypass the slow call to QTreeView::dataChanged. + // QTreeView::dataChanged will invalidate the height cache and that's what we don't need in MessageView. + QAbstractItemView::dataChanged(topLeft, bottomRight, roles); +} + +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()); + } + } + int width = delegate->widthForBytes(max_bytes); + if (header()->sectionSize(5) != width) { + header()->resizeSection(5, width); + } +} diff --git a/tools/cabana/messageswidget.h b/tools/cabana/messageswidget.h index 08dac0d69a..2dc0d1d316 100644 --- a/tools/cabana/messageswidget.h +++ b/tools/cabana/messageswidget.h @@ -1,12 +1,13 @@ #pragma once #include +#include #include #include #include -#include +#include -#include "tools/cabana/dbcmanager.h" +#include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" class MessageListModel : public QAbstractTableModel { @@ -34,14 +35,24 @@ private: Qt::SortOrder sort_order = Qt::AscendingOrder; }; +class MessageView : public QTreeView { + Q_OBJECT +public: + MessageView(QWidget *parent) : QTreeView(parent) {} + void drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void drawBranches(QPainter *painter, const QRect &rect, const QModelIndex &index) const override {} + void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles = QVector()) override; + void updateBytesSectionSize(); +}; + class MessagesWidget : public QWidget { Q_OBJECT public: MessagesWidget(QWidget *parent); void selectMessage(const MessageId &message_id); - QByteArray saveHeaderState() const { return table_widget->horizontalHeader()->saveState(); } - bool restoreHeaderState(const QByteArray &state) const { return table_widget->horizontalHeader()->restoreState(state); } + QByteArray saveHeaderState() const { return view->header()->saveState(); } + bool restoreHeaderState(const QByteArray &state) const { return view->header()->restoreState(state); } void updateSuppressedButtons(); void reset(); @@ -49,11 +60,11 @@ signals: void msgSelectionChanged(const MessageId &message_id); protected: - QTableView *table_widget; + MessageView *view; std::optional current_msg_id; QLineEdit *filter; + QCheckBox *multiple_lines_bytes; MessageListModel *model; QPushButton *suppress_add; QPushButton *suppress_clear; - }; diff --git a/tools/cabana/route.cc b/tools/cabana/route.cc deleted file mode 100644 index ab322cdf90..0000000000 --- a/tools/cabana/route.cc +++ /dev/null @@ -1,68 +0,0 @@ -#include "tools/cabana/route.h" - -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/streams/replaystream.h" - -OpenRouteDialog::OpenRouteDialog(QWidget *parent) : QDialog(parent) { - // TODO: get route list from api.comma.ai - QHBoxLayout *edit_layout = new QHBoxLayout; - edit_layout->addWidget(new QLabel(tr("Route:"))); - edit_layout->addWidget(route_edit = new QLineEdit(this)); - route_edit->setPlaceholderText(tr("Enter remote route name or click browse to select a local route")); - auto file_btn = new QPushButton(tr("Browse..."), this); - edit_layout->addWidget(file_btn); - - btn_box = new QDialogButtonBox(QDialogButtonBox::Open | QDialogButtonBox::Cancel); - btn_box->button(QDialogButtonBox::Open)->setEnabled(false); - - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->addStretch(0); - main_layout->addLayout(edit_layout); - main_layout->addStretch(0); - main_layout->addWidget(btn_box); - setMinimumSize({550, 120}); - - QObject::connect(btn_box, &QDialogButtonBox::accepted, this, &OpenRouteDialog::loadRoute); - QObject::connect(btn_box, &QDialogButtonBox::rejected, this, &QDialog::reject); - QObject::connect(route_edit, &QLineEdit::textChanged, [this]() { - btn_box->button(QDialogButtonBox::Open)->setEnabled(!route_edit->text().isEmpty()); - }); - QObject::connect(file_btn, &QPushButton::clicked, [=]() { - QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), settings.last_route_dir); - if (!dir.isEmpty()) { - route_edit->setText(dir); - settings.last_route_dir = QFileInfo(dir).absolutePath(); - } - }); -} - -void OpenRouteDialog::loadRoute() { - btn_box->setEnabled(false); - - QString route = route_edit->text(); - QString data_dir; - if (int idx = route.lastIndexOf('/'); idx != -1) { - data_dir = route.mid(0, idx + 1); - route = route.mid(idx + 1); - } - - bool is_valid_format = Route::parseRoute(route).str.size() > 0; - if (!is_valid_format) { - QMessageBox::warning(nullptr, tr("Warning"), tr("Invalid route format: '%1'").arg(route)); - } else { - failed_to_load = !dynamic_cast(can)->loadRoute(route, data_dir); - if (failed_to_load) { - QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to load route: '%1'").arg(route)); - } else { - accept(); - } - } - - btn_box->setEnabled(true); -} diff --git a/tools/cabana/route.h b/tools/cabana/route.h deleted file mode 100644 index ceda71d585..0000000000 --- a/tools/cabana/route.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include -#include - -class OpenRouteDialog : public QDialog { - Q_OBJECT - -public: - OpenRouteDialog(QWidget *parent); - void loadRoute(); - inline bool failedToLoad() const { return failed_to_load; } - -private: - QLineEdit *route_edit; - QDialogButtonBox *btn_box; - bool failed_to_load = false; -}; diff --git a/tools/cabana/settings.cc b/tools/cabana/settings.cc index 00bbb94de5..f759324988 100644 --- a/tools/cabana/settings.cc +++ b/tools/cabana/settings.cc @@ -1,11 +1,16 @@ #include "tools/cabana/settings.h" +#include #include #include +#include #include +#include #include +#include + +#include "tools/cabana/util.h" -// Settings Settings settings; Settings::Settings() { @@ -25,8 +30,14 @@ void Settings::save() { s.setValue("geometry", geometry); s.setValue("video_splitter_state", video_splitter_state); s.setValue("recent_files", recent_files); - s.setValue("message_header_state", message_header_state); + 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); } void Settings::load() { @@ -42,15 +53,32 @@ void Settings::load() { 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").toByteArray(); + 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(); + if (log_path.isEmpty()) { + log_path = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/cabana_live_stream/"; + } } // SettingsDlg SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { setWindowTitle(tr("Settings")); - QFormLayout *form_layout = new QFormLayout(this); + QVBoxLayout *main_layout = new QVBoxLayout(this); + QGroupBox *groupbox = new QGroupBox("General"); + QFormLayout *form_layout = new QFormLayout(groupbox); + + 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); fps->setRange(10, 100); @@ -63,7 +91,18 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { 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); + 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); chart_series_type->addItems({tr("Line"), tr("Step Line"), tr("Scatter")}); chart_series_type->setCurrentIndex(settings.chart_series_type); @@ -74,21 +113,56 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { 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); + log_livestream->setCheckable(true); + QHBoxLayout *path_layout = new QHBoxLayout(log_livestream); + path_layout->addWidget(log_path = new QLineEdit(settings.log_path, this)); + log_path->setReadOnly(true); + auto browse_btn = new QPushButton(tr("B&rowse...")); + path_layout->addWidget(browse_btn); + main_layout->addWidget(log_livestream); + - auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - form_layout->addRow(buttonBox); + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Apply); + main_layout->addWidget(buttonBox); + main_layout->addStretch(1); - setFixedWidth(360); - connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDlg::save); - connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + QObject::connect(browse_btn, &QPushButton::clicked, [this]() { + QString fn = QFileDialog::getExistingDirectory( + this, tr("Log File Location"), + QStandardPaths::writableLocation(QStandardPaths::HomeLocation), + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + if (!fn.isEmpty()) { + 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(); + } + }); } 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.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(); - accept(); emit settings.changed(); } diff --git a/tools/cabana/settings.h b/tools/cabana/settings.h index c3ba906576..11548ef5cc 100644 --- a/tools/cabana/settings.h +++ b/tools/cabana/settings.h @@ -1,14 +1,27 @@ #pragma once #include +#include #include #include +#include +#include #include +#define LIGHT_THEME 1 +#define DARK_THEME 2 + class Settings : public QObject { Q_OBJECT public: + enum DragDirection { + MsbFirst, + LsbFirst, + AlwaysLE, + AlwaysBE, + }; + Settings(); void save(); void load(); @@ -17,8 +30,13 @@ public: int max_cached_minutes = 30; int chart_height = 200; int chart_column_count = 1; - int chart_range = 3 * 60; // e minutes + int chart_range = 3 * 60; // 3 minutes int chart_series_type = 0; + int theme = 0; + int sparkline_range = 15; // 15 seconds + bool multiple_lines_bytes = true; + bool log_livestream = true; + QString log_path; QString last_dir; QString last_route_dir; QByteArray geometry; @@ -26,6 +44,7 @@ public: QByteArray window_state; QStringList recent_files; QByteArray message_header_state; + DragDirection drag_direction; signals: void changed(); @@ -41,6 +60,10 @@ public: QSpinBox *cached_minutes; QSpinBox *chart_height; QComboBox *chart_series_type; + QComboBox *theme; + QGroupBox *log_livestream; + QLineEdit *log_path; + QComboBox *drag_direction; }; extern Settings settings; diff --git a/tools/cabana/signaledit.cc b/tools/cabana/signalview.cc similarity index 69% rename from tools/cabana/signaledit.cc rename to tools/cabana/signalview.cc index d91f6c1401..32b0a39408 100644 --- a/tools/cabana/signaledit.cc +++ b/tools/cabana/signalview.cc @@ -1,12 +1,17 @@ -#include "tools/cabana/signaledit.h" +#include "tools/cabana/signalview.h" #include #include #include #include #include +#include #include +#include +#include #include +#include +#include #include #include "tools/cabana/commands.h" @@ -20,7 +25,6 @@ SignalModel::SignalModel(QObject *parent) : root(new Item), QAbstractItemModel(p QObject::connect(dbc(), &DBCManager::signalAdded, this, &SignalModel::handleSignalAdded); QObject::connect(dbc(), &DBCManager::signalUpdated, this, &SignalModel::handleSignalUpdated); QObject::connect(dbc(), &DBCManager::signalRemoved, this, &SignalModel::handleSignalRemoved); - QObject::connect(can, &AbstractStream::msgsReceived, this, &SignalModel::updateState); } void SignalModel::insertItem(SignalModel::Item *parent_item, int pos, const cabana::Signal *sig) { @@ -36,7 +40,6 @@ void SignalModel::setMessage(const MessageId &id) { msg_id = id; filter_str = ""; refresh(); - updateState(nullptr); } void SignalModel::setFilter(const QString &txt) { @@ -57,24 +60,6 @@ void SignalModel::refresh() { endResetModel(); } -void SignalModel::updateState(const QHash *msgs) { - if (!msgs || msgs->contains(msg_id)) { - auto &dat = can->lastMessage(msg_id).dat; - int row = 0; - for (auto item : root->children) { - QString value = QString::number(get_raw_value((uint8_t *)dat.begin(), dat.size(), *item->sig), 'f', item->sig->precision); - if (!item->sig->unit.isEmpty()){ - value += " " + item->sig->unit; - } - if (value != item->sig_val) { - item->sig_val = value; - emit dataChanged(index(row, 1), index(row, 1), {Qt::DisplayRole}); - } - ++row; - } - } -} - SignalModel::Item *SignalModel::getItem(const QModelIndex &index) const { SignalModel::Item *item = nullptr; if (index.isValid()) { @@ -160,6 +145,8 @@ QVariant SignalModel::data(const QModelIndex &index, int role) const { if (item->type == Item::Signed) return item->sig->is_signed ? Qt::Checked : Qt::Unchecked; } else if (role == Qt::DecorationRole && index.column() == 0 && item->type == Item::ExtraInfo) { return utils::icon(item->parent->extra_expanded ? "chevron-compact-down" : "chevron-compact-up"); + } else if (role == Qt::ToolTipRole && item->type == Item::Sig) { + return (index.column() == 0) ? item->sig->name : item->sig_val; } } return {}; @@ -237,19 +224,13 @@ bool SignalModel::saveSignal(const cabana::Signal *origin_s, cabana::Signal &s) void SignalModel::addSignal(int start_bit, int size, bool little_endian) { auto msg = dbc()->msg(msg_id); - for (int i = 0; !msg; ++i) { - QString name = QString("NEW_MSG_") + QString::number(msg_id.address, 16).toUpper(); - if (!dbc()->msg(msg_id.source, name)) { - UndoStack::push(new EditMsgCommand(msg_id, name, can->lastMessage(msg_id).dat.size())); - msg = dbc()->msg(msg_id); - } + if (!msg) { + QString name = dbc()->newMsgName(msg_id); + UndoStack::push(new EditMsgCommand(msg_id, name, can->lastMessage(msg_id).dat.size())); + msg = dbc()->msg(msg_id); } - cabana::Signal sig = {.is_little_endian = little_endian, .factor = 1, .min = "0", .max = QString::number(std::pow(2, size) - 1)}; - for (int i = 1; /**/; ++i) { - sig.name = QString("NEW_SIGNAL_%1").arg(i); - if (msg->sig(sig.name) == nullptr) break; - } + cabana::Signal sig = {.name = dbc()->newSignalName(msg_id), .is_little_endian = little_endian, .factor = 1, .min = "0", .max = QString::number(std::pow(2, size) - 1)}; updateSigSizeParamsFromRange(sig, start_bit, size); UndoStack::push(new AddSigCommand(msg_id, sig)); } @@ -262,6 +243,9 @@ void SignalModel::resizeSignal(const cabana::Signal *sig, int start_bit, int siz void SignalModel::removeSignal(const cabana::Signal *sig) { UndoStack::push(new RemoveSigCommand(msg_id, sig)); + if (dbc()->signalCount(msg_id) == 0) { + UndoStack::push(new RemoveMsgCommand(msg_id)); + } } void SignalModel::handleMsgChanged(MessageId id) { @@ -302,7 +286,8 @@ SignalItemDelegate::SignalItemDelegate(QObject *parent) : QStyledItemDelegate(pa name_validator = new NameValidator(this); double_validator = new QDoubleValidator(this); double_validator->setLocale(QLocale::C); // Match locale of QString::toDouble() instead of system - small_font.setPointSize(8); + label_font.setPointSize(8); + minmax_font.setPixelSize(10); } QSize SignalItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { @@ -314,50 +299,88 @@ QSize SignalItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QMo int spacing = option.widget->style()->pixelMetric(QStyle::PM_TreeViewIndentation) + color_label_width + 8; it = width_cache.insert(text, option.fontMetrics.width(text) + spacing); } - width = std::min(width, it.value()); + width = std::min(option.widget->size().width() / 3.0, it.value()); } return {width, QApplication::fontMetrics().height()}; } +bool SignalItemDelegate::helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) { + if (event && event->type() == QEvent::ToolTip && index.isValid()) { + auto item = (SignalModel::Item *)index.internalPointer(); + if (item->type == SignalModel::Item::Sig && index.column() == 1) { + QRect rc = option.rect.adjusted(0, 0, -option.rect.width() * 0.4, 0); + if (rc.contains(event->pos())) { + event->setAccepted(false); + return false; + } + } + } + return QStyledItemDelegate::helpEvent(event, view, option, index); +} + +void SignalItemDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const { + auto item = (SignalModel::Item *)index.internalPointer(); + if (editor && item->type == SignalModel::Item::Sig && index.column() == 1) { + QRect geom = option.rect; + geom.setLeft(geom.right() - editor->sizeHint().width()); + editor->setGeometry(geom); + button_size = geom.size(); + return; + } + QStyledItemDelegate::updateEditorGeometry(editor, option, index); +} + void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { auto item = (SignalModel::Item *)index.internalPointer(); - if (index.column() == 0 && item && item->type == SignalModel::Item::Sig) { - painter->save(); + if (item && item->type == SignalModel::Item::Sig) { painter->setRenderHint(QPainter::Antialiasing); if (option.state & QStyle::State_Selected) { painter->fillRect(option.rect, option.palette.highlight()); } - // color label - auto bg_color = getColor(item->sig); int h_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; int v_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameVMargin); - QRect rc{option.rect.left() + h_margin, option.rect.top(), color_label_width, option.rect.height()}; - painter->setPen(Qt::NoPen); - painter->setBrush(item->highlight ? bg_color.darker(125) : bg_color); - painter->drawRoundedRect(rc.adjusted(0, v_margin, 0, -v_margin), 3, 3); - painter->setPen(item->highlight ? Qt::white : Qt::black); - painter->setFont(small_font); - painter->drawText(rc, Qt::AlignCenter, QString::number(item->row() + 1)); - - // signal name - painter->setFont(option.font); - painter->setPen(option.palette.color(option.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text)); - QString text = index.data(Qt::DisplayRole).toString(); - QRect text_rect = option.rect; - text_rect.setLeft(rc.right() + h_margin * 2); - text = painter->fontMetrics().elidedText(text, Qt::ElideRight, text_rect.width()); - painter->drawText(text_rect, option.displayAlignment, text); - painter->restore(); - } else if (index.column() == 1 && item && item->type == SignalModel::Item::Sig) { - // draw signal value - if (option.state & QStyle::State_Selected) { - painter->fillRect(option.rect, option.palette.highlight()); + QRect r = option.rect.adjusted(h_margin, v_margin, -h_margin, -v_margin); + if (index.column() == 0) { + // color label + QPainterPath path; + QRect icon_rect{r.x(), r.y(), color_label_width, r.height()}; + path.addRoundedRect(icon_rect, 3, 3); + painter->setPen(item->highlight ? Qt::white : Qt::black); + painter->setFont(label_font); + painter->fillPath(path, getColor(item->sig).darker(item->highlight ? 125 : 0)); + painter->drawText(icon_rect, Qt::AlignCenter, QString::number(item->row() + 1)); + + r.setLeft(icon_rect.right() + h_margin * 2); + auto text = option.fontMetrics.elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, r.width()); + painter->setPen(option.palette.color(option.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text)); + painter->setFont(option.font); + painter->drawText(r, option.displayAlignment, text); + } else if (index.column() == 1) { + // sparkline + QSize sparkline_size = item->sparkline.pixmap.size() / item->sparkline.pixmap.devicePixelRatio(); + painter->drawPixmap(QRect(r.topLeft(), sparkline_size), item->sparkline.pixmap); + // min-max value + 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) { + painter->drawLine(rect.topLeft(), rect.bottomLeft()); + rect.adjust(5, -v_margin, 0, v_margin); + painter->setFont(minmax_font); + QString min = QString::number(item->sparkline.min_val); + QString max = QString::number(item->sparkline.max_val); + painter->drawText(rect, Qt::AlignLeft | Qt::AlignTop, max); + painter->drawText(rect, Qt::AlignLeft | Qt::AlignBottom, min); + QFontMetrics fm(minmax_font); + value_adjust = std::max(fm.width(min), fm.width(max)) + 5; + } + // value + painter->setFont(option.font); + rect.adjust(value_adjust, 0, -button_size.width(), 0); + auto text = option.fontMetrics.elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, rect.width()); + painter->drawText(rect, Qt::AlignRight | Qt::AlignVCenter, text); } - painter->setPen(option.palette.color(option.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text)); - QRect rc = option.rect.adjusted(0, 0, -70, 0); - auto text = painter->fontMetrics().elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, rc.width()); - painter->drawText(rc, Qt::AlignRight | Qt::AlignVCenter, text); } else { QStyledItemDelegate::paint(painter, option, index); } @@ -410,14 +433,25 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), filter_edit->setPlaceholderText(tr("filter signals")); hl->addWidget(filter_edit); hl->addStretch(1); - auto collapse_btn = toolButton("dash-square", tr("Collapse All")); + + // WARNING: increasing the maximum range can result in severe performance degradation. + // 30s is a reasonable value at present. + const int max_range = 30; // 30s + settings.sparkline_range = std::clamp(settings.sparkline_range, 1, max_range); + hl->addWidget(sparkline_label = new QLabel()); + hl->addWidget(sparkline_range_slider = new QSlider(Qt::Horizontal, this)); + sparkline_range_slider->setRange(1, max_range); + sparkline_range_slider->setValue(settings.sparkline_range); + sparkline_range_slider->setToolTip(tr("Sparkline time range")); + + auto collapse_btn = new ToolButton("dash-square", tr("Collapse All")); collapse_btn->setIconSize({12, 12}); hl->addWidget(collapse_btn); // tree view - tree = new QTreeView(this); + tree = new TreeView(this); tree->setModel(model = new SignalModel(this)); - tree->setItemDelegate(new SignalItemDelegate(this)); + tree->setItemDelegate(delegate = new SignalItemDelegate(this)); tree->setFrameShape(QFrame::NoFrame); tree->setHeaderHidden(true); tree->setMouseTracking(true); @@ -432,16 +466,21 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), main_layout->setSpacing(0); main_layout->addWidget(title_bar); main_layout->addWidget(tree); + updateToolBar(); QObject::connect(filter_edit, &QLineEdit::textEdited, model, &SignalModel::setFilter); + QObject::connect(sparkline_range_slider, &QSlider::valueChanged, this, &SignalView::setSparklineRange); QObject::connect(collapse_btn, &QPushButton::clicked, tree, &QTreeView::collapseAll); QObject::connect(tree, &QAbstractItemView::clicked, this, &SignalView::rowClicked); QObject::connect(tree, &QTreeView::viewportEntered, [this]() { emit highlight(nullptr); }); QObject::connect(tree, &QTreeView::entered, [this](const QModelIndex &index) { emit highlight(model->getItem(index)->sig); }); QObject::connect(model, &QAbstractItemModel::modelReset, this, &SignalView::rowsChanged); - QObject::connect(model, &QAbstractItemModel::rowsInserted, this, &SignalView::rowsChanged); QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &SignalView::rowsChanged); QObject::connect(dbc(), &DBCManager::signalAdded, [this](MessageId id, const cabana::Signal *sig) { selectSignal(sig); }); + QObject::connect(can, &AbstractStream::msgsReceived, this, &SignalView::updateState); + QObject::connect(dbc(), &DBCManager::signalUpdated, this, &SignalView::handleSignalUpdated); + QObject::connect(tree->verticalScrollBar(), &QScrollBar::valueChanged, [this]() { updateState(); }); + QObject::connect(tree->verticalScrollBar(), &QScrollBar::rangeChanged, [this]() { updateState(); }); setWhatsThis(tr(R"( Signal view
@@ -450,7 +489,7 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), } void SignalView::setMessage(const MessageId &id) { - msg_id = id; + max_value_width = 0; filter_edit->clear(); model->setMessage(id); } @@ -465,10 +504,9 @@ void SignalView::rowsChanged() { int h_margin = style()->pixelMetric(QStyle::PM_FocusFrameHMargin); h->setContentsMargins(0, v_margin, -h_margin, v_margin); h->setSpacing(style()->pixelMetric(QStyle::PM_ToolBarItemSpacing)); - h->addStretch(0); - auto remove_btn = toolButton("x", tr("Remove signal")); - auto plot_btn = toolButton("graph-up", ""); + auto remove_btn = new ToolButton("x", tr("Remove signal")); + auto plot_btn = new ToolButton("graph-up", ""); plot_btn->setCheckable(true); h->addWidget(plot_btn); h->addWidget(remove_btn); @@ -477,12 +515,13 @@ void SignalView::rowsChanged() { auto sig = model->getItem(index)->sig; QObject::connect(remove_btn, &QToolButton::clicked, [=]() { model->removeSignal(sig); }); QObject::connect(plot_btn, &QToolButton::clicked, [=](bool checked) { - emit showChart(msg_id, sig, checked, QGuiApplication::keyboardModifiers() & Qt::ShiftModifier); + emit showChart(model->msg_id, sig, checked, QGuiApplication::keyboardModifiers() & Qt::ShiftModifier); }); } } - signal_count_lb->setText(tr("Signals: %1").arg(model->rowCount())); + updateToolBar(); updateChartState(); + updateState(); } void SignalView::rowClicked(const QModelIndex &index) { @@ -509,7 +548,7 @@ void SignalView::selectSignal(const cabana::Signal *sig, bool expand) { void SignalView::updateChartState() { int i = 0; for (auto item : model->root->children) { - bool chart_opened = charts->hasSignal(msg_id, item->sig); + bool chart_opened = charts->hasSignal(model->msg_id, item->sig); auto buttons = tree->indexWidget(model->index(i, 1))->findChildren(); if (buttons.size() > 0) { buttons[0]->setChecked(chart_opened); @@ -525,8 +564,83 @@ void SignalView::signalHovered(const cabana::Signal *sig) { bool highlight = children[i]->sig == sig; if (std::exchange(children[i]->highlight, highlight) != highlight) { emit model->dataChanged(model->index(i, 0), model->index(i, 0), {Qt::DecorationRole}); + emit model->dataChanged(model->index(i, 1), model->index(i, 1), {Qt::DisplayRole}); + } + } +} + +void SignalView::updateToolBar() { + signal_count_lb->setText(tr("Signals: %1").arg(model->rowCount())); + sparkline_label->setText(utils::formatSeconds(settings.sparkline_range)); +} + +void SignalView::setSparklineRange(int value) { + settings.sparkline_range = value; + updateToolBar(); + updateState(); +} + +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; + updateState(); + } +} + +void SignalView::updateState(const QHash *msgs) { + if (model->rowCount() == 0 || (msgs && !msgs->contains(model->msg_id))) return; + + const auto &last_msg = can->lastMessage(model->msg_id); + for (auto item : model->root->children) { + double value = get_raw_value((uint8_t *)last_msg.dat.constData(), last_msg.dat.size(), *item->sig); + item->sig_val = QString::number(value, 'f', item->sig->precision); + // Show unit + if (!item->sig->unit.isEmpty()) { + item->sig_val += " " + item->sig->unit; + } + // Show enum string + for (auto &[val, desc] : item->sig->val_desc) { + if (std::abs(value - val.toInt()) < 1e-6) { + item->sig_val = desc; + } + } + max_value_width = std::max(max_value_width, fontMetrics().width(item->sig_val)); + } + + // update visible sparkline + QSize size(tree->columnWidth(1) - delegate->button_size.width(), delegate->button_size.height()); + int min_max_width = std::min(size.width() - 10, QFontMetrics(delegate->minmax_font).width("-000.00") + 5); + int value_width = std::min(max_value_width, size.width() * 0.35); + size -= {value_width + min_max_width, style()->pixelMetric(QStyle::PM_FocusFrameVMargin) * 2}; + + QModelIndex top = tree->indexAt(QPoint(0, 0)); + QModelIndex bottom = tree->indexAt(tree->viewport()->rect().bottomLeft()); + int start_row = top.parent().isValid() ? top.parent().row() + 1 : top.row(); + int end_row = model->rowCount() - 1; + if (bottom.isValid()) { + end_row = bottom.parent().isValid() ? bottom.parent().row() : bottom.row(); + } + QFutureSynchronizer synchronizer; + for (int i = start_row; i <= end_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.waitForFinished(); + + for (int i = 0; i < model->rowCount(); ++i) { + emit model->dataChanged(model->index(i, 1), model->index(i, 1), {Qt::DisplayRole}); + } +} + +void SignalView::resizeEvent(QResizeEvent* event) { + updateState(); + QFrame::resizeEvent(event); } void SignalView::leaveEvent(QEvent *event) { diff --git a/tools/cabana/signaledit.h b/tools/cabana/signalview.h similarity index 71% rename from tools/cabana/signaledit.h rename to tools/cabana/signalview.h index ae73a7b3bc..02741234a6 100644 --- a/tools/cabana/signaledit.h +++ b/tools/cabana/signalview.h @@ -3,11 +3,13 @@ #include #include #include +#include #include #include #include -#include "tools/cabana/chartswidget.h" +#include "tools/cabana/chart/chartswidget.h" +#include "tools/cabana/chart/sparkline.h" class SignalModel : public QAbstractItemModel { Q_OBJECT @@ -26,6 +28,7 @@ public: bool highlight = false; bool extra_expanded = false; QString sig_val = "-"; + Sparkline sparkline; }; SignalModel(QObject *parent); @@ -53,12 +56,12 @@ private: void handleSignalRemoved(const cabana::Signal *sig); void handleMsgChanged(MessageId id); void refresh(); - void updateState(const QHash *msgs); MessageId msg_id; QString filter_str; std::unique_ptr root; friend class SignalView; + friend class SignalItemDelegate; }; class ValueDescriptionDlg : public QDialog { @@ -82,9 +85,13 @@ public: void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) override; + void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QValidator *name_validator, *double_validator; - QFont small_font; + QFont label_font, minmax_font; const int color_label_width = 18; + mutable QSize button_size; mutable QHash width_cache; }; @@ -106,11 +113,32 @@ signals: private: void rowsChanged(); - void leaveEvent(QEvent *event); - - MessageId msg_id; - QTreeView *tree; + void leaveEvent(QEvent *event) override; + void resizeEvent(QResizeEvent* event) override; + void updateToolBar(); + void setSparklineRange(int value); + void handleSignalUpdated(const cabana::Signal *sig); + void updateState(const QHash *msgs = nullptr); + + struct TreeView : public QTreeView { + TreeView(QWidget *parent) : QTreeView(parent) {} + void rowsInserted(const QModelIndex &parent, int start, int end) override { + ((SignalView *)parentWidget())->rowsChanged(); + // update widget geometries in QTreeView::rowsInserted + QTreeView::rowsInserted(parent, start, end); + } + void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles = QVector()) override { + // Bypass the slow call to QTreeView::dataChanged. + QAbstractItemView::dataChanged(topLeft, bottomRight, roles); + } + }; + int max_value_width = 0; + TreeView *tree; + QLabel *sparkline_label; + QSlider *sparkline_range_slider; QLineEdit *filter_edit; ChartsWidget *charts; QLabel *signal_count_lb; + SignalItemDelegate *delegate; + friend SignalItemDelegate; }; diff --git a/tools/cabana/streams/abstractstream.cc b/tools/cabana/streams/abstractstream.cc index d60beed0c8..88c56dce43 100644 --- a/tools/cabana/streams/abstractstream.cc +++ b/tools/cabana/streams/abstractstream.cc @@ -3,16 +3,21 @@ AbstractStream *can = nullptr; -AbstractStream::AbstractStream(QObject *parent, bool is_live_streaming) : is_live_streaming(is_live_streaming), QObject(parent) { +AbstractStream::AbstractStream(QObject *parent) : QObject(parent) { can = this; new_msgs = std::make_unique>(); - QObject::connect(this, &AbstractStream::received, this, &AbstractStream::process, Qt::QueuedConnection); QObject::connect(this, &AbstractStream::seekedTo, this, &AbstractStream::updateLastMsgsTo); } -void AbstractStream::process(QHash *messages) { +void AbstractStream::updateMessages(QHash *messages) { + auto prev_src_size = sources.size(); for (auto it = messages->begin(); it != messages->end(); ++it) { - last_msgs[it.key()] = it.value(); + const auto &id = it.key(); + last_msgs[id] = it.value(); + sources.insert(id.source); + } + if (sources.size() != prev_src_size) { + emit sourcesUpdated(sources); } emit updated(); emit msgsReceived(messages); @@ -20,92 +25,91 @@ void AbstractStream::process(QHash *messages) { processing = false; } -bool AbstractStream::updateEvent(const Event *event) { - static double prev_update_ts = 0; - - if (event->which == cereal::Event::Which::CAN) { - double current_sec = event->mono_time / 1e9 - routeStartTime(); - for (const auto &c : event->event.getCan()) { - MessageId id = {.source = c.getSrc(), .address = c.getAddress()}; - CanData &data = (*new_msgs)[id]; - data.ts = current_sec; - data.dat = QByteArray((char *)c.getDat().begin(), c.getDat().size()); - data.count = ++counters[id]; - data.freq = data.count / std::max(1.0, current_sec); - - auto &tracker = change_trackers[id]; - tracker.compute(data.dat, data.ts, data.freq); - data.colors = tracker.colors; - data.last_change_t = tracker.last_change_t; - data.bit_change_counts = tracker.bit_change_counts; - - if (!sources.contains(id.source)) { - sources.insert(id.source); - emit sourcesUpdated(sources); - } - } +void AbstractStream::updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size) { + all_msgs[id].compute((const char*)data, size, sec, getSpeed()); + if (!new_msgs->contains(id)) { + new_msgs->insert(id, {}); + } +} - double ts = millis_since_boot(); - if ((ts - prev_update_ts) > (1000.0 / settings.fps) && !processing && !new_msgs->isEmpty()) { - // delay posting CAN message if UI thread is busy - processing = true; - prev_update_ts = ts; - // use pointer to avoid data copy in queued connection. - emit received(new_msgs.release()); - new_msgs.reset(new QHash); - new_msgs->reserve(100); +bool AbstractStream::postEvents() { + // delay posting CAN message if UI thread is busy + if (!processing) { + 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); + new_msgs->reserve(100); + return true; } - return true; + return false; } const CanData &AbstractStream::lastMessage(const MessageId &id) { - static CanData empty_data; + static CanData empty_data = {}; auto it = last_msgs.find(id); return it != last_msgs.end() ? it.value() : empty_data; } // it is thread safe to update data in updateLastMsgsTo. -// updateEvent will not be called before replayStream::seekedTo return. +// updateLastMsgsTo is always called in UI thread. void AbstractStream::updateLastMsgsTo(double sec) { - new_msgs->clear(); - change_trackers.clear(); + new_msgs.reset(new QHash); + all_msgs.clear(); last_msgs.clear(); - counters.clear(); - - CanEvent last_event = {.mono_time = uint64_t((sec + routeStartTime()) * 1e9)}; - for (auto &[id, e] : events_) { - auto it = std::lower_bound(e.crbegin(), e.crend(), last_event, std::greater()); - if (it != e.crend()) { - auto &m = last_msgs[id]; - m.dat = QByteArray((const char *)it->dat, it->size); - m.ts = it->mono_time / 1e9 - routeStartTime(); - m.count = std::distance(it, e.crend()); - m.freq = m.count / std::max(1.0, m.ts); - m.last_change_t = QVector(m.dat.size(), m.ts); - m.colors = QVector(m.dat.size(), QColor(0, 0, 0, 0)); - m.bit_change_counts = QVector>(m.dat.size()); - counters[id] = m.count; + + 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; + }); + if (it != ev.crend()) { + double ts = (*it)->mono_time / 1e9 - routeStartTime(); + auto &m = all_msgs[id]; + m.compute((const char *)(*it)->dat, (*it)->size, ts, getSpeed()); + m.count = std::distance(it, ev.crend()); + m.freq = m.count / std::max(1.0, ts); } } + last_msgs = all_msgs; + // use a timer to prevent recursive calls QTimer::singleShot(0, [this]() { emit updated(); emit msgsReceived(&last_msgs); }); } -void AbstractStream::parseEvents(std::unordered_map> &msgs, +void AbstractStream::parseEvents(std::unordered_map> &msgs, std::vector::const_iterator first, std::vector::const_iterator last) { - for (; first != last; ++first) { - if ((*first)->which == cereal::Event::Which::CAN) { - for (const auto &c : (*first)->event.getCan()) { + size_t memory_size = 0; + for (auto it = first; it != last; ++it) { + if ((*it)->which == cereal::Event::Which::CAN) { + for (const auto &c : (*it)->event.getCan()) { + memory_size += sizeof(CanEvent) + sizeof(uint8_t) * c.getDat().size(); + } + } + } + + char *ptr = memory_blocks.emplace_back(new char[memory_size]).get(); + 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()) { + CanEvent *e = (CanEvent *)ptr; + e->src = c.getSrc(); + e->address = c.getAddress(); + e->mono_time = ts; auto dat = c.getDat(); - auto &m = msgs[{.source = c.getSrc(), .address = c.getAddress()}].emplace_back(); - m.size = std::min(dat.size(), std::size(m.dat)); - memcpy(m.dat, (uint8_t *)dat.begin(), m.size); - m.mono_time = (*first)->mono_time; + e->size = dat.size(); + memcpy(e->dat, (uint8_t *)dat.begin(), e->size); + + msgs[{.source = e->src, .address = e->address}].push_back(e); + all_events_.push_back(e); + ptr += sizeof(CanEvent) + sizeof(uint8_t) * e->size; } - last_event_ts = std::max(last_event_ts, (*first)->mono_time); } } } @@ -116,13 +120,93 @@ void AbstractStream::mergeEvents(std::vector::const_iterator first, std if (append) { parseEvents(events_, first, last); } else { - std::unordered_map> new_events; + std::unordered_map> new_events; parseEvents(new_events, first, last); for (auto &[id, new_e] : new_events) { auto &e = events_[id]; - auto it = std::upper_bound(e.cbegin(), e.cend(), new_e.front()); + auto it = std::upper_bound(e.cbegin(), e.cend(), new_e.front(), [](const CanEvent *l, const CanEvent *r) { + return l->mono_time < r->mono_time; + }); e.insert(it, new_e.cbegin(), new_e.cend()); } } + total_sec = (all_events_.back()->mono_time - all_events_.front()->mono_time) / 1e9; emit eventsMerged(); } + +// CanData + +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); + +static 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); +} + +void CanData::compute(const char *can_data, const int size, double current_sec, double playback_speed, uint32_t in_freq) { + ts = current_sec; + ++count; + freq = in_freq == 0 ? count / std::max(1.0, current_sec) : in_freq; + if (dat.size() != size) { + dat.resize(size); + bit_change_counts.resize(size); + colors = QVector(size, QColor(0, 0, 0, 0)); + last_change_t = QVector(size, ts); + last_delta.resize(size); + same_delta_counter.resize(size); + } 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; + + for (int i = 0; i < size; ++i) { + const uint8_t last = dat[i]; + const uint8_t cur = can_data[i]; + const int delta = cur - last; + + if (last != cur) { + double delta_t = ts - last_change_t[i]; + + // 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); + } else { + same_delta_counter[i] = std::max(0, same_delta_counter[i] - 4); + } + + // Mostly moves in the same direction, color based on delta up/down + if (delta_t * freq > periodic_threshold || same_delta_counter[i] > 8) { + // Last change was while ago, choose color based on delta up or down + colors[i] = (cur > last) ? cyan : red; + } else { + // Periodic changes + colors[i] = blend(colors[i], 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; + } + } + + last_change_t[i] = ts; + last_delta[i] = 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)); + } + } + } + memcpy(dat.data(), can_data, size); +} diff --git a/tools/cabana/streams/abstractstream.h b/tools/cabana/streams/abstractstream.h index 3c7bdd6132..644aa133d4 100644 --- a/tools/cabana/streams/abstractstream.h +++ b/tools/cabana/streams/abstractstream.h @@ -1,58 +1,63 @@ #pragma once +#include #include #include #include #include #include -#include "tools/cabana/dbcmanager.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 char *dat, const int size, double current_sec, double playback_speed, uint32_t in_freq = 0); + double ts = 0.; uint32_t count = 0; - uint32_t freq = 0; + double freq = 0; QByteArray dat; QVector colors; QVector last_change_t; QVector> bit_change_counts; + QVector last_delta; + QVector same_delta_counter; }; struct CanEvent { + uint8_t src; + uint32_t address; uint64_t mono_time; uint8_t size; - uint8_t dat[64]; - inline bool operator<(const CanEvent &r) const { return mono_time < r.mono_time; } - inline bool operator>(const CanEvent &r) const { return mono_time > r.mono_time; } + uint8_t dat[]; }; class AbstractStream : public QObject { Q_OBJECT public: - AbstractStream(QObject *parent, bool is_live_streaming); + AbstractStream(QObject *parent); virtual ~AbstractStream() {}; - inline bool liveStreaming() const { return is_live_streaming; } - inline double lastEventSecond() const { return last_event_ts / 1e9 - routeStartTime(); } + inline bool liveStreaming() const { return route() == nullptr; } + inline double lastEventSecond() const { return lastEventMonoTime() / 1e9 - routeStartTime(); } virtual void seekTo(double ts) {} virtual QString routeName() const = 0; virtual QString carFingerprint() const { return ""; } - virtual double totalSeconds() const { return 0; } virtual double routeStartTime() const { return 0; } virtual double currentSec() const = 0; - virtual QDateTime currentDateTime() const { return {}; } - virtual const CanData &lastMessage(const MessageId &id); + double totalSeconds() const { return total_sec; } + 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 std::unordered_map> &events() const { return events_; } + const std::deque &allEvents() const { return all_events_; } + const std::deque &events(const MessageId &id) const { return events_.at(id); } virtual const std::vector> getTimeline() { return {}; } - void mergeEvents(std::vector::const_iterator first, std::vector::const_iterator last, bool append); signals: void paused(); @@ -62,26 +67,39 @@ signals: void eventsMerged(); void updated(); void msgsReceived(const QHash *); - void received(QHash *); - void sourcesUpdated(const QSet &s); + void sourcesUpdated(const SourceSet &s); public: QHash last_msgs; - QSet sources; + SourceSet sources; protected: - virtual void process(QHash *); - bool updateEvent(const Event *event); + void mergeEvents(std::vector::const_iterator first, std::vector::const_iterator last, bool append); + bool postEvents(); + uint64_t lastEventMonoTime() const { return all_events_.empty() ? 0 : all_events_.back()->mono_time; } + void updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size); + void updateMessages(QHash *); + void parseEvents(std::unordered_map> &msgs, std::vector::const_iterator first, std::vector::const_iterator last); void updateLastMsgsTo(double sec); - void parseEvents(std::unordered_map> &msgs, std::vector::const_iterator first, std::vector::const_iterator last); - bool is_live_streaming = false; + double total_sec = 0; std::atomic processing = false; - QHash counters; std::unique_ptr> new_msgs; - QHash change_trackers; - std::unordered_map> events_; - uint64_t last_event_ts = 0; + QHash all_msgs; + std::unordered_map> events_; + std::deque all_events_; + std::deque> memory_blocks; +}; + +class AbstractOpenStreamWidget : public QWidget { + Q_OBJECT +public: + AbstractOpenStreamWidget(AbstractStream **stream, QWidget *parent = nullptr) : stream(stream), QWidget(parent) {} + virtual bool open() = 0; + virtual QString title() = 0; + +protected: + AbstractStream **stream = nullptr; }; // A global pointer referring to the unique AbstractStream object diff --git a/tools/cabana/streams/devicestream.cc b/tools/cabana/streams/devicestream.cc new file mode 100644 index 0000000000..5bbe527773 --- /dev/null +++ b/tools/cabana/streams/devicestream.cc @@ -0,0 +1,72 @@ +#include "tools/cabana/streams/devicestream.h" + +#include +#include +#include +#include +#include + +// DeviceStream + +DeviceStream::DeviceStream(QObject *parent, QString address) : zmq_address(address), LiveStream(parent) { + startStreamThread(); +} + +void DeviceStream::streamThread() { + if (!zmq_address.isEmpty()) { + setenv("ZMQ", "1", 1); + } + + std::unique_ptr context(Context::create()); + std::string address = zmq_address.isEmpty() ? "127.0.0.1" : zmq_address.toStdString(); + std::unique_ptr sock(SubSocket::create(context.get(), "can", address)); + assert(sock != NULL); + sock->setTimeout(50); + // run as fast as messages come in + while (!QThread::currentThread()->isInterruptionRequested()) { + Message *msg = sock->receive(true); + if (!msg) { + QThread::msleep(50); + continue; + } + + handleEvent(msg->getData(), msg->getSize()); + delete msg; + } +} + +AbstractOpenStreamWidget *DeviceStream::widget(AbstractStream **stream) { + return new OpenDeviceWidget(stream); +} + +// OpenDeviceWidget + +OpenDeviceWidget::OpenDeviceWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) { + QRadioButton *msgq = new QRadioButton(tr("MSGQ")); + QRadioButton *zmq = new QRadioButton(tr("ZMQ")); + ip_address = new QLineEdit(this); + ip_address->setPlaceholderText(tr("Enter device Ip Address")); + QString ip_range = "(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])"; + QString pattern("^" + ip_range + "\\." + ip_range + "\\." + ip_range + "\\." + ip_range + "$"); + QRegularExpression re(pattern); + ip_address->setValidator(new QRegularExpressionValidator(re, this)); + + group = new QButtonGroup(this); + group->addButton(msgq, 0); + group->addButton(zmq, 1); + + QFormLayout *form_layout = new QFormLayout(this); + form_layout->addRow(msgq); + form_layout->addRow(zmq, ip_address); + QObject::connect(group, qOverload(&QButtonGroup::buttonToggled), [=](QAbstractButton *button, bool checked) { + ip_address->setEnabled(button == zmq && checked); + }); + zmq->setChecked(true); +} + +bool OpenDeviceWidget::open() { + QString ip = ip_address->text().isEmpty() ? "127.0.0.1" : ip_address->text(); + bool msgq = group->checkedId() == 0; + *stream = new DeviceStream(qApp, msgq ? "" : ip); + return true; +} diff --git a/tools/cabana/streams/devicestream.h b/tools/cabana/streams/devicestream.h new file mode 100644 index 0000000000..a65f073458 --- /dev/null +++ b/tools/cabana/streams/devicestream.h @@ -0,0 +1,30 @@ +#pragma once + +#include "tools/cabana/streams/livestream.h" + +class DeviceStream : public LiveStream { + Q_OBJECT +public: + DeviceStream(QObject *parent, QString address = {}); + static AbstractOpenStreamWidget *widget(AbstractStream **stream); + inline QString routeName() const override { + return QString("Live Streaming From %1").arg(zmq_address.isEmpty() ? "127.0.0.1" : zmq_address); + } + +protected: + void streamThread() override; + const QString zmq_address; +}; + +class OpenDeviceWidget : public AbstractOpenStreamWidget { + Q_OBJECT + +public: + OpenDeviceWidget(AbstractStream **stream); + bool open() override; + QString title() override { return tr("&Device"); } + +private: + QLineEdit *ip_address; + QButtonGroup *group; +}; diff --git a/tools/cabana/streams/livestream.cc b/tools/cabana/streams/livestream.cc index 62ad635152..466537e9b8 100644 --- a/tools/cabana/streams/livestream.cc +++ b/tools/cabana/streams/livestream.cc @@ -2,85 +2,118 @@ #include -LiveStream::LiveStream(QObject *parent, QString address) : zmq_address(address), AbstractStream(parent, true) { +LiveStream::LiveStream(QObject *parent) : AbstractStream(parent) { + if (settings.log_livestream) { + std::string path = (settings.log_path + "/" + QDateTime::currentDateTime().toString("yyyy-MM-dd--hh-mm-ss") + "--0").toStdString(); + util::create_directories(path, 0755); + fs.reset(new std::ofstream(path + "/rlog", std::ios::binary | std::ios::out)); + } stream_thread = new QThread(this); + + QObject::connect(&settings, &Settings::changed, this, &LiveStream::startUpdateTimer); QObject::connect(stream_thread, &QThread::started, [=]() { streamThread(); }); QObject::connect(stream_thread, &QThread::finished, stream_thread, &QThread::deleteLater); +} + +void LiveStream::startUpdateTimer() { + update_timer.stop(); + update_timer.start(1000.0 / settings.fps, this); + timer_id = update_timer.timerId(); +} + +void LiveStream::startStreamThread() { + // delay the start of the thread to avoid calling startStreamThread + // in the constructor when other classes' slots have not been connected to + // the signals of the livestream. QTimer::singleShot(0, [this]() { stream_thread->start(); }); + startUpdateTimer(); } LiveStream::~LiveStream() { + update_timer.stop(); stream_thread->requestInterruption(); stream_thread->quit(); stream_thread->wait(); } -void LiveStream::streamThread() { - if (!zmq_address.isEmpty()) { - setenv("ZMQ", "1", 1); +// called in streamThread +void LiveStream::handleEvent(const char *data, const size_t size) { + if (fs) { + fs->write(data, size); } - std::unique_ptr context(Context::create()); - std::string address = zmq_address.isEmpty() ? "127.0.0.1" : zmq_address.toStdString(); - std::unique_ptr sock(SubSocket::create(context.get(), "can", address)); - assert(sock != NULL); - sock->setTimeout(50); - // run as fast as messages come in - while (!QThread::currentThread()->isInterruptionRequested()) { - Message *msg = sock->receive(true); - if (!msg) { - QThread::msleep(50); - continue; + + std::lock_guard lk(lock); + auto &msg = receivedMessages.emplace_back(data, size); + receivedEvents.push_back(msg.event); +} + +void LiveStream::timerEvent(QTimerEvent *event) { + if (event->timerId() == timer_id) { + { + // merge events received from live stream thread. + std::lock_guard lk(lock); + mergeEvents(receivedEvents.cbegin(), receivedEvents.cend(), true); + receivedEvents.clear(); + receivedMessages.clear(); + } + if (!all_events_.empty()) { + begin_event_ts = all_events_.front()->mono_time; + updateEvents(); + return; } - std::lock_guard lk(lock); - handleEvent(messages.emplace_back(msg).event); - // TODO: write stream to log file to replay it with cabana --data_dir flag. } + QObject::timerEvent(event); } -void LiveStream::handleEvent(Event *evt) { - if (start_ts == 0 || evt->mono_time < start_ts) { - if (evt->mono_time < start_ts) { - qDebug() << "stream is looping back to old time stamp"; - } - start_ts = current_ts = evt->mono_time; +void LiveStream::updateEvents() { + static double prev_speed = 1.0; + static uint64_t prev_newest_event_ts = all_events_.back()->mono_time; + + if (first_update_ts == 0) { + first_update_ts = nanos_since_boot(); + first_event_ts = current_event_ts = all_events_.back()->mono_time; emit streamStarted(); } - received.push_back(evt); - if (!pause_) { - if (speed_ < 1 && last_update_ts > 0) { - auto it = std::upper_bound(received.cbegin(), received.cend(), current_ts, [](uint64_t ts, auto &e) { - return ts < e->mono_time; - }); - if (it != received.cend()) { - bool skip = (nanos_since_boot() - last_update_ts) < ((*it)->mono_time - current_ts) / speed_; - if (skip) return; - - evt = *it; - } - } - current_ts = evt->mono_time; - last_update_ts = nanos_since_boot(); - updateEvent(evt); + if (paused_ || prev_speed != speed_) { + prev_speed = speed_; + first_update_ts = nanos_since_boot(); + first_event_ts = current_event_ts; + return; } -} -void LiveStream::process(QHash *last_messages) { - { - std::lock_guard lk(lock); - auto first = std::upper_bound(received.cbegin(), received.cend(), last_event_ts, [](uint64_t ts, auto &e) { - return ts < e->mono_time; - }); - mergeEvents(first, received.cend(), true); - if (speed_ == 1) { - received.clear(); - messages.clear(); - } + uint64_t last_event_ts = all_events_.back()->mono_time; + bool at_the_end = current_event_ts == prev_newest_event_ts; + if (!at_the_end) { + last_event_ts = first_event_ts + (nanos_since_boot() - first_update_ts) * speed_; } - AbstractStream::process(last_messages); + + auto first = std::upper_bound(all_events_.cbegin(), all_events_.cend(), current_event_ts, [](uint64_t ts, auto e) { + return ts < e->mono_time; + }); + auto last = std::upper_bound(first, all_events_.cend(), last_event_ts, [](uint64_t ts, auto e) { + return ts < e->mono_time; + }); + + for (auto it = first; it != last; ++it) { + const CanEvent *e = *it; + MessageId id = {.source = e->src, .address = e->address}; + updateEvent(id, (e->mono_time - begin_event_ts) / 1e9, e->dat, e->size); + current_event_ts = e->mono_time; + } + postEvents(); + prev_newest_event_ts = all_events_.back()->mono_time; +} + +void LiveStream::seekTo(double sec) { + sec = std::max(0.0, sec); + first_update_ts = nanos_since_boot(); + first_event_ts = std::min(sec * 1e9 + begin_event_ts, lastEventMonoTime()); + current_event_ts = first_event_ts; + emit seekedTo((current_event_ts - begin_event_ts) / 1e9); } void LiveStream::pause(bool pause) { - pause_ = pause; - emit paused(); + paused_ = pause; + emit(pause ? paused() : resume()); } diff --git a/tools/cabana/streams/livestream.h b/tools/cabana/streams/livestream.h index 598c0b9365..197ac64e3e 100644 --- a/tools/cabana/streams/livestream.h +++ b/tools/cabana/streams/livestream.h @@ -1,46 +1,55 @@ #pragma once +#include + #include "tools/cabana/streams/abstractstream.h" class LiveStream : public AbstractStream { Q_OBJECT public: - LiveStream(QObject *parent, QString address = {}); + LiveStream(QObject *parent); virtual ~LiveStream(); - inline QString routeName() const override { - return QString("Live Streaming From %1").arg(zmq_address.isEmpty() ? "127.0.0.1" : zmq_address); - } - inline double routeStartTime() const override { return start_ts / (double)1e9; } - inline double currentSec() const override { return (current_ts - start_ts) / (double)1e9; } - void setSpeed(float speed) override { speed_ = std::min(1.0, speed); } - bool isPaused() const override { return pause_; } + inline double routeStartTime() const override { return begin_event_ts / 1e9; } + inline double currentSec() const override { return (current_event_ts - begin_event_ts) / 1e9; } + void setSpeed(float speed) override { speed_ = speed; } + double getSpeed() override { return speed_; } + bool isPaused() const override { return paused_; } void pause(bool pause) override; + void seekTo(double sec) override; protected: - virtual void handleEvent(Event *evt); - virtual void streamThread(); - void process(QHash *) override; + virtual void streamThread() = 0; + void startStreamThread(); + void handleEvent(const char *data, const size_t size); + +private: + void startUpdateTimer(); + void timerEvent(QTimerEvent *event) override; + void updateEvents(); struct Msg { - Msg(Message *m) { - event = ::new Event(aligned_buf.align(m)); - delete m; + Msg(const char *data, const size_t size) { + event = ::new Event(aligned_buf.align(data, size)); } ~Msg() { ::delete event; } Event *event; AlignedBuffer aligned_buf; }; - mutable std::mutex lock; - std::vector received; - std::deque messages; - std::atomic start_ts = 0; - std::atomic current_ts = 0; - std::atomic speed_ = 1; - std::atomic pause_ = false; - uint64_t last_update_ts = 0; - - const QString zmq_address; + std::mutex lock; QThread *stream_thread; + std::vector receivedEvents; + std::deque receivedMessages; + + std::unique_ptr fs; + int timer_id; + QBasicTimer update_timer; + + uint64_t begin_event_ts = 0; + uint64_t current_event_ts = 0; + uint64_t first_event_ts = 0; + uint64_t first_update_ts = 0; + double speed_ = 1; + bool paused_ = false; }; diff --git a/tools/cabana/streams/pandastream.cc b/tools/cabana/streams/pandastream.cc new file mode 100644 index 0000000000..6eab40a652 --- /dev/null +++ b/tools/cabana/streams/pandastream.cc @@ -0,0 +1,116 @@ +#include "tools/cabana/streams/pandastream.h" + +#include +#include +#include + +PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(config_), LiveStream(parent) { + if (config.serial.isEmpty()) { + auto serials = Panda::list(); + if (serials.size() == 0) { + throw std::runtime_error("No panda found"); + } + config.serial = QString::fromStdString(serials[0]); + } + + qDebug() << "Connecting to panda with serial" << config.serial; + if (!connect()) { + throw std::runtime_error("Failed to connect to panda"); + } + startStreamThread(); +} + +bool PandaStream::connect() { + try { + panda.reset(new Panda(config.serial.toStdString())); + config.bus_config.resize(3); + qDebug() << "Connected"; + } catch (const std::exception& e) { + return false; + } + + panda->set_safety_model(cereal::CarParams::SafetyModel::SILENT); + + for (int bus = 0; bus < config.bus_config.size(); bus++) { + panda->set_can_speed_kbps(bus, config.bus_config[bus].can_speed_kbps); + + // CAN-FD + if (panda->hw_type == cereal::PandaState::PandaType::RED_PANDA || panda->hw_type == cereal::PandaState::PandaType::RED_PANDA_V2) { + if (config.bus_config[bus].can_fd) { + panda->set_data_speed_kbps(bus, config.bus_config[bus].data_speed_kbps); + } else { + // Hack to disable can-fd by setting data speed to a low value + panda->set_data_speed_kbps(bus, 10); + } + } + + } + return true; +} + +void PandaStream::streamThread() { + std::vector raw_can_data; + + while (!QThread::currentThread()->isInterruptionRequested()) { + QThread::msleep(1); + + if (!panda->connected()) { + qDebug() << "Connection to panda lost. Attempting reconnect."; + if (!connect()){ + QThread::msleep(1000); + continue; + } + } + + raw_can_data.clear(); + if (!panda->can_receive(raw_can_data)) { + qDebug() << "failed to receive"; + continue; + } + + MessageBuilder msg; + auto evt = msg.initEvent(); + auto canData = evt.initCan(raw_can_data.size()); + + for (uint i = 0; isend_heartbeat(false); + } +} + +AbstractOpenStreamWidget *PandaStream::widget(AbstractStream **stream) { + return new OpenPandaWidget(stream); +} + +// OpenPandaWidget + +OpenPandaWidget::OpenPandaWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->addStretch(1); + + QFormLayout *form_layout = new QFormLayout(); + form_layout->addRow(tr("Serial"), serial_edit = new QLineEdit(this)); + serial_edit->setPlaceholderText(tr("Leave empty to use default serial")); + + main_layout->addLayout(form_layout); + main_layout->addStretch(1); +} + +bool OpenPandaWidget::open() { + try { + PandaStreamConfig config = {.serial = serial_edit->text()}; + *stream = new PandaStream(qApp, config); + } catch (std::exception &e) { + QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to panda: '%1'").arg(e.what())); + return false; + } + return true; +} diff --git a/tools/cabana/streams/pandastream.h b/tools/cabana/streams/pandastream.h new file mode 100644 index 0000000000..df7ccded51 --- /dev/null +++ b/tools/cabana/streams/pandastream.h @@ -0,0 +1,44 @@ +#pragma once + +#include "tools/cabana/streams/livestream.h" +#include "selfdrive/boardd/panda.h" + +struct BusConfig { + int can_speed_kbps = 500; + int data_speed_kbps = 2000; + bool can_fd = false; +}; + +struct PandaStreamConfig { + QString serial = ""; + std::vector bus_config; +}; + +class PandaStream : public LiveStream { + Q_OBJECT +public: + PandaStream(QObject *parent, PandaStreamConfig config_ = {}); + static AbstractOpenStreamWidget *widget(AbstractStream **stream); + inline QString routeName() const override { + return QString("Live Streaming From Panda %1").arg(config.serial); + } + +protected: + void streamThread() override; + bool connect(); + + std::unique_ptr panda; + PandaStreamConfig config = {}; +}; + +class OpenPandaWidget : public AbstractOpenStreamWidget { + Q_OBJECT + +public: + OpenPandaWidget(AbstractStream **stream); + bool open() override; + QString title() override { return tr("&Panda"); } + +private: + QLineEdit *serial_edit; +}; diff --git a/tools/cabana/streams/replaystream.cc b/tools/cabana/streams/replaystream.cc index 140a795d3a..68e46c77da 100644 --- a/tools/cabana/streams/replaystream.cc +++ b/tools/cabana/streams/replaystream.cc @@ -1,6 +1,14 @@ #include "tools/cabana/streams/replaystream.h" -ReplayStream::ReplayStream(uint32_t replay_flags, QObject *parent) : replay_flags(replay_flags), AbstractStream(parent, false) { +#include +#include +#include +#include +#include + +#include "common/prefix.h" + +ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent) { QObject::connect(&settings, &Settings::changed, [this]() { if (replay) replay->setSegmentCacheLimit(settings.max_cached_minutes); }); @@ -25,7 +33,7 @@ void ReplayStream::mergeSegments() { } } -bool ReplayStream::loadRoute(const QString &route, const QString &data_dir) { +bool ReplayStream::loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags) { replay.reset(new Replay(route, {"can", "roadEncodeIdx", "wideRoadEncodeIdx", "carParams"}, {}, nullptr, replay_flags, data_dir, this)); replay->setSegmentCacheLimit(settings.max_cached_minutes); replay->installEventFilter(event_filter, this); @@ -33,11 +41,6 @@ bool ReplayStream::loadRoute(const QString &route, const QString &data_dir) { QObject::connect(replay.get(), &Replay::streamStarted, this, &AbstractStream::streamStarted); QObject::connect(replay.get(), &Replay::segmentsMerged, this, &ReplayStream::mergeSegments); if (replay->load()) { - const auto &segments = replay->route()->segments(); - if (std::none_of(segments.begin(), segments.end(), [](auto &s) { return s.second.rlog.length() > 0; })) { - qWarning() << "no rlogs in route" << route; - return false; - } replay->start(); return true; } @@ -45,8 +48,21 @@ bool ReplayStream::loadRoute(const QString &route, const QString &data_dir) { } bool ReplayStream::eventFilter(const Event *event) { + static double prev_update_ts = 0; + // delay posting CAN message if UI thread is busy if (event->which == cereal::Event::Which::CAN) { - updateEvent(event); + double current_sec = event->mono_time / 1e9 - routeStartTime(); + for (const auto &c : event->event.getCan()) { + MessageId id = {.source = c.getSrc(), .address = c.getAddress()}; + const auto dat = c.getDat(); + updateEvent(id, current_sec, (const uint8_t*)dat.begin(), dat.size()); + } + double ts = millis_since_boot(); + if ((ts - prev_update_ts) > (1000.0 / settings.fps)) { + if (postEvents()) { + prev_update_ts = ts; + } + } } return true; } @@ -55,3 +71,74 @@ void ReplayStream::pause(bool pause) { replay->pause(pause); emit(pause ? paused() : resume()); } + + +AbstractOpenStreamWidget *ReplayStream::widget(AbstractStream **stream) { + return new OpenReplayWidget(stream); +} + +// OpenReplayWidget + +static std::unique_ptr op_prefix; + +OpenReplayWidget::OpenReplayWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) { + // TODO: get route list from api.comma.ai + QGridLayout *grid_layout = new QGridLayout(); + grid_layout->addWidget(new QLabel(tr("Route")), 0, 0); + grid_layout->addWidget(route_edit = new QLineEdit(this), 0, 1); + route_edit->setPlaceholderText(tr("Enter remote route name or click browse to select a local route")); + auto file_btn = new QPushButton(tr("Browse..."), this); + grid_layout->addWidget(file_btn, 0, 2); + + grid_layout->addWidget(new QLabel(tr("Video")), 1, 0); + grid_layout->addWidget(choose_video_cb = new QComboBox(this), 1, 1); + QString items[] = {tr("No Video"), tr("Road Camera"), tr("Wide Road Camera"), tr("Driver Camera"), tr("QCamera")}; + for (int i = 0; i < std::size(items); ++i) { + choose_video_cb->addItem(items[i]); + } + choose_video_cb->setCurrentIndex(1); // default is road camera; + + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->addLayout(grid_layout); + setMinimumWidth(550); + + QObject::connect(file_btn, &QPushButton::clicked, [=]() { + QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), settings.last_route_dir); + if (!dir.isEmpty()) { + route_edit->setText(dir); + settings.last_route_dir = QFileInfo(dir).absolutePath(); + } + }); +} + +bool OpenReplayWidget::open() { + QString route = route_edit->text(); + QString data_dir; + if (int idx = route.lastIndexOf('/'); idx != -1) { + data_dir = route.mid(0, idx + 1); + route = route.mid(idx + 1); + } + + bool ret = false; + bool is_valid_format = Route::parseRoute(route).str.size() > 0; + if (!is_valid_format) { + QMessageBox::warning(nullptr, tr("Warning"), tr("Invalid route format: '%1'").arg(route)); + } else { + // TODO: Remove when OpenpilotPrefix supports ZMQ +#ifndef __APPLE__ + op_prefix.reset(new OpenpilotPrefix()); +#endif + uint32_t flags[] = {REPLAY_FLAG_NO_VIPC, REPLAY_FLAG_NONE, REPLAY_FLAG_ECAM, REPLAY_FLAG_DCAM, REPLAY_FLAG_QCAMERA}; + ReplayStream *replay_stream = *stream ? (ReplayStream *)*stream : new ReplayStream(qApp); + ret = replay_stream->loadRoute(route, data_dir, flags[choose_video_cb->currentIndex()]); + if (!ret) { + if (replay_stream != *stream) { + delete replay_stream; + } + QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to load route: '%1'").arg(route)); + } else { + *stream = replay_stream; + } + } + return ret; +} diff --git a/tools/cabana/streams/replaystream.h b/tools/cabana/streams/replaystream.h index 6fc61f7f36..5b8e018fc9 100644 --- a/tools/cabana/streams/replaystream.h +++ b/tools/cabana/streams/replaystream.h @@ -1,33 +1,44 @@ #pragma once #include "tools/cabana/streams/abstractstream.h" -#include "tools/cabana/settings.h" class ReplayStream : public AbstractStream { Q_OBJECT public: - ReplayStream(uint32_t replay_flags, QObject *parent); + ReplayStream(QObject *parent); ~ReplayStream(); - bool loadRoute(const QString &route, const QString &data_dir); + bool loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE); bool eventFilter(const Event *event); void seekTo(double ts) override { replay->seekTo(std::max(double(0), ts), false); }; inline QString routeName() const override { return replay->route()->name(); } inline QString carFingerprint() const override { return replay->carFingerprint().c_str(); } inline VisionStreamType visionStreamType() const override { return replay->hasFlag(REPLAY_FLAG_ECAM) ? VISION_STREAM_WIDE_ROAD : VISION_STREAM_ROAD; } - inline double totalSeconds() const override { return replay->totalSeconds(); } inline double routeStartTime() const override { return replay->routeStartTime() / (double)1e9; } inline double currentSec() const override { return replay->currentSeconds(); } - inline QDateTime currentDateTime() const override { return replay->currentDateTime(); } inline const Route *route() const override { return replay->route(); } inline void setSpeed(float speed) override { replay->setSpeed(speed); } + inline float getSpeed() const { return replay->getSpeed(); } inline bool isPaused() const override { return replay->isPaused(); } void pause(bool pause) override; inline const std::vector> getTimeline() override { return replay->getTimeline(); } + static AbstractOpenStreamWidget *widget(AbstractStream **stream); private: void mergeSegments(); std::unique_ptr replay = nullptr; - uint32_t replay_flags = REPLAY_FLAG_NONE; std::set processed_segments; }; + +class OpenReplayWidget : public AbstractOpenStreamWidget { + Q_OBJECT + +public: + OpenReplayWidget(AbstractStream **stream); + bool open() override; + QString title() override { return tr("&Replay"); } + +private: + QLineEdit *route_edit; + QComboBox *choose_video_cb; +}; diff --git a/tools/cabana/streamselector.cc b/tools/cabana/streamselector.cc new file mode 100644 index 0000000000..bfd6ff24d9 --- /dev/null +++ b/tools/cabana/streamselector.cc @@ -0,0 +1,52 @@ +#include "tools/cabana/streamselector.h" + +#include +#include +#include +#include +#include + +StreamSelector::StreamSelector(QWidget *parent) : QDialog(parent) { + setWindowTitle(tr("Open stream")); + QVBoxLayout *main_layout = new QVBoxLayout(this); + + tab = new QTabWidget(this); + tab->setTabBarAutoHide(true); + main_layout->addWidget(tab); + + QHBoxLayout *dbc_layout = new QHBoxLayout(); + dbc_file = new QLineEdit(this); + dbc_file->setReadOnly(true); + dbc_file->setPlaceholderText(tr("Choose a dbc file to open")); + QPushButton *file_btn = new QPushButton(tr("Browse...")); + dbc_layout->addWidget(new QLabel(tr("dbc File"))); + dbc_layout->addWidget(dbc_file); + dbc_layout->addWidget(file_btn); + main_layout->addLayout(dbc_layout); + + QFrame *line = new QFrame(this); + line->setFrameStyle(QFrame::HLine | QFrame::Sunken); + main_layout->addWidget(line); + + auto btn_box = new QDialogButtonBox(QDialogButtonBox::Open | QDialogButtonBox::Cancel); + main_layout->addWidget(btn_box); + + QObject::connect(btn_box, &QDialogButtonBox::rejected, this, &QDialog::reject); + QObject::connect(btn_box, &QDialogButtonBox::accepted, [=]() { + success = ((AbstractOpenStreamWidget *)tab->currentWidget())->open(); + if (success) { + accept(); + } + }); + QObject::connect(file_btn, &QPushButton::clicked, [this]() { + QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), settings.last_dir, "DBC (*.dbc)"); + if (!fn.isEmpty()) { + dbc_file->setText(fn); + settings.last_dir = QFileInfo(fn).absolutePath(); + } + }); +} + +void StreamSelector::addStreamWidget(AbstractOpenStreamWidget *w) { + tab->addTab(w, w->title()); +} diff --git a/tools/cabana/streamselector.h b/tools/cabana/streamselector.h new file mode 100644 index 0000000000..ecd0e8530c --- /dev/null +++ b/tools/cabana/streamselector.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +#include "tools/cabana/streams/abstractstream.h" + +class StreamSelector : public QDialog { + Q_OBJECT + +public: + StreamSelector(QWidget *parent = nullptr); + void addStreamWidget(AbstractOpenStreamWidget *w); + QString dbcFile() const { return dbc_file->text(); } + inline bool failed() const { return !success; } + +private: + QLineEdit *dbc_file; + QTabWidget *tab; + bool success = true; +}; diff --git a/tools/cabana/tests/test_cabana b/tools/cabana/tests/test_cabana deleted file mode 100755 index bac242fbdd..0000000000 --- a/tools/cabana/tests/test_cabana +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -cd "$(dirname "$0")" -export LD_LIBRARY_PATH="../../../opendbc/can:$LD_LIBRARY_PATH" -exec ./_test_cabana "$1" diff --git a/tools/cabana/tests/test_cabana.cc b/tools/cabana/tests/test_cabana.cc index 8445867acb..4bc01a6a81 100644 --- a/tools/cabana/tests/test_cabana.cc +++ b/tools/cabana/tests/test_cabana.cc @@ -3,21 +3,20 @@ #undef INFO #include "catch2/catch.hpp" #include "tools/replay/logreader.h" -#include "tools/cabana/dbcmanager.h" +#include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" // demo route, first segment const std::string TEST_RLOG_URL = "https://commadata2.blob.core.windows.net/commadata2/4cf7a6ad03080c90/2021-09-29--13-46-36/0/rlog.bz2"; -TEST_CASE("DBCManager::generateDBC") { - DBCManager dbc_origin(nullptr); - dbc_origin.open("toyota_new_mc_pt_generated"); - DBCManager dbc_from_generated(nullptr); - dbc_from_generated.open("", dbc_origin.generateDBC()); +TEST_CASE("DBCFile::generateDBC") { + QString fn = QString("%1/%2.dbc").arg(OPENDBC_FILE_PATH, "toyota_new_mc_pt_generated"); + DBCFile dbc_origin(fn); + DBCFile dbc_from_generated("", dbc_origin.generateDBC()); REQUIRE(dbc_origin.msgCount() == dbc_from_generated.msgCount()); - auto msgs = dbc_origin.getMessages(0); - auto new_msgs = dbc_from_generated.getMessages(0); + auto msgs = dbc_origin.getMessages(); + auto new_msgs = dbc_from_generated.getMessages(); for (auto &[id, m] : msgs) { auto &new_m = new_msgs.at(id); REQUIRE(m.name == new_m.name); @@ -33,7 +32,7 @@ TEST_CASE("DBCManager::generateDBC") { TEST_CASE("Parse can messages") { DBCManager dbc(nullptr); - dbc.open("toyota_new_mc_pt_generated"); + dbc.open({0}, "toyota_new_mc_pt_generated"); CANParser can_parser(0, "toyota_new_mc_pt_generated", {}, {}); LogReader log; diff --git a/tools/cabana/tools/findsimilarbits.cc b/tools/cabana/tools/findsimilarbits.cc index ffd0df0501..9d81ebd5d0 100644 --- a/tools/cabana/tools/findsimilarbits.cc +++ b/tools/cabana/tools/findsimilarbits.cc @@ -1,5 +1,6 @@ #include "tools/cabana/tools/findsimilarbits.h" +#include #include #include #include @@ -7,7 +8,7 @@ #include #include -#include "tools/cabana/dbcmanager.h" +#include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" FindSimilarBitsDlg::FindSimilarBitsDlg(QWidget *parent) : QDialog(parent, Qt::WindowFlags() | Qt::Window) { @@ -16,20 +17,23 @@ FindSimilarBitsDlg::FindSimilarBitsDlg(QWidget *parent) : QDialog(parent, Qt::Wi QVBoxLayout *main_layout = new QVBoxLayout(this); - QHBoxLayout *form_layout = new QHBoxLayout(); - bus_combo = new QComboBox(this); - QSet bus_set; + QHBoxLayout *src_layout = new QHBoxLayout(); + src_bus_combo = new QComboBox(this); + find_bus_combo = new QComboBox(this); + SourceSet bus_set; for (auto it = can->last_msgs.begin(); it != can->last_msgs.end(); ++it) { bus_set << it.key().source; } - for (uint8_t bus : bus_set) { - bus_combo->addItem(QString::number(bus), bus); + for (auto cb : {src_bus_combo, find_bus_combo}) { + for (uint8_t bus : bus_set) { + cb->addItem(QString::number(bus), bus); + } + cb->model()->sort(0); + cb->setCurrentIndex(0); } - bus_combo->model()->sort(0); - bus_combo->setCurrentIndex(0); msg_cb = new QComboBox(this); - // TODO: update when bus_combo changes + // TODO: update when src_bus_combo changes for (auto &[id, msg] : dbc()->getMessages(0)) { msg_cb->addItem(msg.name, id.address); } @@ -37,29 +41,44 @@ FindSimilarBitsDlg::FindSimilarBitsDlg(QWidget *parent) : QDialog(parent, Qt::Wi msg_cb->setCurrentIndex(0); byte_idx_sb = new QSpinBox(this); + byte_idx_sb->setFixedWidth(50); byte_idx_sb->setRange(0, 63); bit_idx_sb = new QSpinBox(this); + bit_idx_sb->setFixedWidth(50); bit_idx_sb->setRange(0, 7); - form_layout->addWidget(new QLabel("Bus")); - form_layout->addWidget(bus_combo); - form_layout->addWidget(msg_cb); - form_layout->addWidget(new QLabel("Byte Index")); - form_layout->addWidget(byte_idx_sb); - form_layout->addWidget(new QLabel("Bit Index")); - form_layout->addWidget(bit_idx_sb); - - + src_layout->addWidget(new QLabel(tr("Bus"))); + src_layout->addWidget(src_bus_combo); + src_layout->addWidget(msg_cb); + src_layout->addWidget(new QLabel(tr("Byte Index"))); + src_layout->addWidget(byte_idx_sb); + src_layout->addWidget(new QLabel(tr("Bit Index"))); + src_layout->addWidget(bit_idx_sb); + src_layout->addStretch(0); + + QHBoxLayout *find_layout = new QHBoxLayout(); + find_layout->addWidget(new QLabel(tr("Bus"))); + find_layout->addWidget(find_bus_combo); + find_layout->addWidget(new QLabel(tr("Equal"))); + equal_combo = new QComboBox(this); + equal_combo->addItems({"Yes", "No"}); + find_layout->addWidget(equal_combo); min_msgs = new QLineEdit(this); min_msgs->setValidator(new QIntValidator(this)); min_msgs->setText("100"); - form_layout->addWidget(new QLabel("Min msg count")); - form_layout->addWidget(min_msgs); + find_layout->addWidget(new QLabel(tr("Min msg count"))); + find_layout->addWidget(min_msgs); search_btn = new QPushButton(tr("&Find"), this); - form_layout->addWidget(search_btn); - form_layout->addStretch(1); - main_layout->addLayout(form_layout); + find_layout->addWidget(search_btn); + find_layout->addStretch(0); + + QGridLayout *grid_layout = new QGridLayout(); + grid_layout->addWidget(new QLabel("Find From:"), 0, 0); + grid_layout->addLayout(src_layout, 0, 1); + grid_layout->addWidget(new QLabel("Find In:"), 1, 0); + grid_layout->addLayout(find_layout, 1, 1); + main_layout->addLayout(grid_layout); table = new QTableWidget(this); table->setSelectionBehavior(QAbstractItemView::SelectRows); @@ -70,10 +89,9 @@ FindSimilarBitsDlg::FindSimilarBitsDlg(QWidget *parent) : QDialog(parent, Qt::Wi setMinimumSize({700, 500}); QObject::connect(search_btn, &QPushButton::clicked, this, &FindSimilarBitsDlg::find); - QObject::connect(table, &QTableWidget::doubleClicked, [this](const QModelIndex &index) { if (index.isValid()) { - MessageId msg_id = {.source = (uint8_t)bus_combo->currentData().toUInt(), .address = table->item(index.row(), 0)->text().toUInt(0, 16)}; + MessageId msg_id = {.source = (uint8_t)find_bus_combo->currentData().toUInt(), .address = table->item(index.row(), 0)->text().toUInt(0, 16)}; emit openMessage(msg_id); } }); @@ -83,7 +101,8 @@ void FindSimilarBitsDlg::find() { search_btn->setEnabled(false); table->clear(); uint32_t selected_address = msg_cb->currentData().toUInt(); - auto msg_mismatched = calcBits(bus_combo->currentText().toUInt(), selected_address, byte_idx_sb->value(), bit_idx_sb->value(), min_msgs->text().toInt()); + auto msg_mismatched = calcBits(src_bus_combo->currentText().toUInt(), selected_address, byte_idx_sb->value(), bit_idx_sb->value(), + find_bus_combo->currentText().toUInt(), equal_combo->currentIndex() == 0, min_msgs->text().toInt()); table->setRowCount(msg_mismatched.size()); table->setColumnCount(6); table->setHorizontalHeaderLabels({"address", "byte idx", "bit idx", "mismatches", "total msgs", "% mismatched"}); @@ -94,33 +113,35 @@ void FindSimilarBitsDlg::find() { table->setItem(i, 2, new QTableWidgetItem(QString::number(m.bit_idx))); table->setItem(i, 3, new QTableWidgetItem(QString::number(m.mismatches))); table->setItem(i, 4, new QTableWidgetItem(QString::number(m.total))); - table->setItem(i, 5, new QTableWidgetItem(QString::number(m.perc))); + table->setItem(i, 5, new QTableWidgetItem(QString::number(m.perc, 'f', 2))); } search_btn->setEnabled(true); } -QList FindSimilarBitsDlg::calcBits(uint8_t bus, uint32_t selected_address, int byte_idx, int bit_idx, int min_msgs_cnt) { +QList FindSimilarBitsDlg::calcBits(uint8_t bus, uint32_t selected_address, int byte_idx, + int bit_idx, uint8_t find_bus, bool equal, int min_msgs_cnt) { QHash> mismatches; QHash msg_count; + const auto &events = can->allEvents(); int bit_to_find = -1; - for (const auto &[id, msg] : can->events()) { - if (id.source == bus) { - for (const auto &c : msg) { - if (id.address == selected_address && c.size > byte_idx) { - bit_to_find = ((c.dat[byte_idx] >> (7 - bit_idx)) & 1) != 0; - } - ++msg_count[id.address]; - if (bit_to_find == -1) continue; + for (const CanEvent *e : events) { + if (e->src == bus) { + if (e->address == selected_address && e->size > byte_idx) { + bit_to_find = ((e->dat[byte_idx] >> (7 - bit_idx)) & 1) != 0; + } + } + if (e->src == find_bus) { + ++msg_count[e->address]; + if (bit_to_find == -1) continue; - auto &mismatched = mismatches[id.address]; - if (mismatched.size() < c.size * 8) { - mismatched.resize(c.size * 8); - } - for (int i = 0; i < c.size; ++i) { - for (int j = 0; j < 8; ++j) { - int bit = ((c.dat[i] >> (7 - j)) & 1) != 0; - mismatched[i * 8 + j] += (bit != bit_to_find); - } + auto &mismatched = mismatches[e->address]; + if (mismatched.size() < e->size * 8) { + mismatched.resize(e->size * 8); + } + for (int i = 0; i < e->size; ++i) { + for (int j = 0; j < 8; ++j) { + int bit = ((e->dat[i] >> (7 - j)) & 1) != 0; + mismatched[i * 8 + j] += equal ? (bit != bit_to_find) : (bit == bit_to_find); } } } diff --git a/tools/cabana/tools/findsimilarbits.h b/tools/cabana/tools/findsimilarbits.h index 53d7806a8f..77bfac19ca 100644 --- a/tools/cabana/tools/findsimilarbits.h +++ b/tools/cabana/tools/findsimilarbits.h @@ -6,7 +6,7 @@ #include #include -#include "tools/cabana/dbcmanager.h" +#include "tools/cabana/dbc/dbcmanager.h" class FindSimilarBitsDlg : public QDialog { Q_OBJECT @@ -22,11 +22,12 @@ private: uint32_t address, byte_idx, bit_idx, mismatches, total; float perc; }; - QList calcBits(uint8_t bus, uint32_t selected_address, int byte_idx, int bit_idx, int min_msgs_cnt); + QList calcBits(uint8_t bus, uint32_t selected_address, int byte_idx, int bit_idx, uint8_t find_bus, + bool equal, int min_msgs_cnt); void find(); QTableWidget *table; - QComboBox *bus_combo, *msg_cb; + QComboBox *src_bus_combo, *find_bus_combo, *msg_cb, *equal_combo; QSpinBox *byte_idx_sb, *bit_idx_sb; QPushButton *search_btn; QLineEdit *min_msgs; diff --git a/tools/cabana/util.cc b/tools/cabana/util.cc index b37b7911ed..7bae2a7bb4 100644 --- a/tools/cabana/util.cc +++ b/tools/cabana/util.cc @@ -1,73 +1,13 @@ #include "tools/cabana/util.h" -#include #include +#include #include #include -#include - -#include -#include +#include #include "selfdrive/ui/qt/util.h" -static QColor blend(QColor a, QColor b) { - return QColor((a.red() + b.red()) / 2, (a.green() + b.green()) / 2, (a.blue() + b.blue()) / 2, (a.alpha() + b.alpha()) / 2); -} - -void ChangeTracker::compute(const QByteArray &dat, double ts, uint32_t freq) { - if (prev_dat.size() != dat.size()) { - colors.resize(dat.size()); - last_change_t.resize(dat.size()); - bit_change_counts.resize(dat.size()); - std::fill(colors.begin(), colors.end(), QColor(0, 0, 0, 0)); - std::fill(last_change_t.begin(), last_change_t.end(), ts); - } else { - for (int i = 0; i < dat.size(); ++i) { - const uint8_t last = prev_dat[i]; - const uint8_t cur = dat[i]; - - if (last != cur) { - double delta_t = ts - last_change_t[i]; - if (delta_t * freq > periodic_threshold) { - // Last change was while ago, choose color based on delta up or down - if (cur > last) { - colors[i] = QColor(0, 187, 255, start_alpha); // Cyan - } else { - colors[i] = QColor(255, 0, 0, start_alpha); // Red - } - } else { - // Periodic changes - colors[i] = blend(colors[i], QColor(102, 86, 169, start_alpha / 2)); // Greyish/Blue - } - - // Track bit level changes - for (int bit = 0; bit < 8; bit++){ - if ((cur ^ last) & (1 << bit)) { - bit_change_counts[i][bit] += 1; - } - } - - last_change_t[i] = ts; - } else { - // Fade out - float alpha_delta = 1.0 / (freq + 1) / fade_time; - colors[i].setAlphaF(std::max(0.0, colors[i].alphaF() - alpha_delta)); - } - } - } - - prev_dat = dat; -} - -void ChangeTracker::clear() { - prev_dat.clear(); - last_change_t.clear(); - bit_change_counts.clear(); - colors.clear(); -} - - // SegmentTree void SegmentTree::build(const QVector &arr) { @@ -103,29 +43,92 @@ std::pair SegmentTree::get_minmax(int n, int left, int right, in // MessageBytesDelegate -MessageBytesDelegate::MessageBytesDelegate(QObject *parent) : QStyledItemDelegate(parent) { +MessageBytesDelegate::MessageBytesDelegate(QObject *parent, bool multiple_lines) : multiple_lines(multiple_lines), QStyledItemDelegate(parent) { fixed_font = QFontDatabase::systemFont(QFontDatabase::FixedFont); - byte_width = QFontMetrics(fixed_font).width("00 "); + byte_size = QFontMetrics(fixed_font).size(Qt::TextSingleLine, "00 ") + QSize(0, 2); +} + +void MessageBytesDelegate::setMultipleLines(bool v) { + if (std::exchange(multiple_lines, v) != multiple_lines) { + std::fill_n(size_cache, std::size(size_cache), QSize{}); + } +} + +int MessageBytesDelegate::widthForBytes(int n) const { + int h_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; + return n * byte_size.width() + h_margin * 2; +} + +QSize MessageBytesDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { + int v_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameVMargin) + 1; + auto data = index.data(BytesRole); + if (!data.isValid()) { + return {1, byte_size.height() + 2 * v_margin}; + } + int n = data.toByteArray().size(); + assert(n >= 0 && n <= 64); + + QSize size = size_cache[n]; + if (size.isEmpty()) { + if (!multiple_lines) { + size.setWidth(widthForBytes(n)); + size.setHeight(byte_size.height() + 2 * v_margin); + } else { + size.setWidth(widthForBytes(8)); + size.setHeight(byte_size.height() * std::max(1, n / 8) + 2 * v_margin); + } + size_cache[n] = size; + } + return size; +} + +bool MessageBytesDelegate::helpEvent(QHelpEvent *e, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) { + if (e->type() == QEvent::ToolTip && index.column() == 0) { + if (view->visualRect(index).width() < QStyledItemDelegate::sizeHint(option, index).width()) { + QToolTip::showText(e->globalPos(), index.data(Qt::DisplayRole).toString(), view); + return true; + } + } + QToolTip::hideText(); + return false; } void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { + auto data = index.data(BytesRole); + if (!data.isValid()) { + return QStyledItemDelegate::paint(painter, option, index); + } + + auto byte_list = data.toByteArray(); auto colors = index.data(ColorsRole).value>(); - auto byte_list = index.data(BytesRole).toByteArray(); int v_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameVMargin); int h_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin); - QRect rc{option.rect.left() + h_margin, option.rect.top() + v_margin, byte_width, option.rect.height() - 2 * v_margin}; + if (option.state & QStyle::State_Selected) { + painter->fillRect(option.rect, option.palette.highlight()); + } - auto color_role = option.state & QStyle::State_Selected ? QPalette::HighlightedText: QPalette::Text; - painter->setPen(option.palette.color(color_role)); + const QPoint pt{option.rect.left() + h_margin, option.rect.top() + v_margin}; + QFont old_font = painter->font(); + QPen old_pen = painter->pen(); painter->setFont(fixed_font); for (int i = 0; i < byte_list.size(); ++i) { + int row = !multiple_lines ? 0 : i / 8; + int column = !multiple_lines ? i : i % 8; + QRect r = QRect({pt.x() + column * byte_size.width(), pt.y() + row * byte_size.height()}, byte_size); if (i < colors.size() && colors[i].alpha() > 0) { - painter->fillRect(rc, colors[i]); + if (option.state & QStyle::State_Selected) { + painter->setPen(option.palette.color(QPalette::Text)); + painter->fillRect(r, option.palette.color(QPalette::Window)); + } + painter->fillRect(r, colors[i]); + } else if (option.state & QStyle::State_Selected) { + painter->setPen(option.palette.color(QPalette::HighlightedText)); } - painter->drawText(rc, Qt::AlignCenter, toHex(byte_list[i])); - rc.moveLeft(rc.right() + 1); + painter->drawText(r, Qt::AlignCenter, toHex(byte_list[i])); } + painter->setFont(old_font); + painter->setPen(old_pen); } QColor getColor(const cabana::Signal *sig) { @@ -139,7 +142,7 @@ QColor getColor(const cabana::Signal *sig) { return QColor::fromHsvF(h, s, v); } -NameValidator::NameValidator(QObject *parent) : QRegExpValidator(QRegExp("^(\\w+)"), parent) { } +NameValidator::NameValidator(QObject *parent) : QRegExpValidator(QRegExp("^(\\w+)"), parent) {} QValidator::State NameValidator::validate(QString &input, int &pos) const { input.replace(' ', '_'); @@ -148,8 +151,7 @@ QValidator::State NameValidator::validate(QString &input, int &pos) const { namespace utils { QPixmap icon(const QString &id) { - static bool dark_theme = QApplication::palette().color(QPalette::WindowText).value() > - QApplication::palette().color(QPalette::Background).value(); + bool dark_theme = settings.theme == DARK_THEME; QPixmap pm; QString key = "bootstrap_" % id % (dark_theme ? "1" : "0"); if (!QPixmapCache::find(key, &pm)) { @@ -157,24 +159,52 @@ QPixmap icon(const QString &id) { if (dark_theme) { QPainter p(&pm); p.setCompositionMode(QPainter::CompositionMode_SourceIn); - p.fillRect(pm.rect(), Qt::lightGray); + p.fillRect(pm.rect(), QColor("#bbbbbb")); } QPixmapCache::insert(key, pm); } return pm; } -} // namespace utils -QToolButton *toolButton(const QString &icon, const QString &tooltip) { - auto btn = new QToolButton(); - btn->setIcon(utils::icon(icon)); - btn->setToolTip(tooltip); - btn->setAutoRaise(true); - const int metric = qApp->style()->pixelMetric(QStyle::PM_SmallIconSize); - btn->setIconSize({metric, metric}); - return btn; -}; +void setTheme(int theme) { + auto style = QApplication::style(); + if (!style) return; + + static int prev_theme = 0; + if (theme != prev_theme) { + prev_theme = theme; + QPalette new_palette; + if (theme == DARK_THEME) { + // "Darcula" like dark theme + new_palette.setColor(QPalette::Window, QColor("#353535")); + new_palette.setColor(QPalette::WindowText, QColor("#bbbbbb")); + new_palette.setColor(QPalette::Base, QColor("#3c3f41")); + new_palette.setColor(QPalette::AlternateBase, QColor("#3c3f41")); + new_palette.setColor(QPalette::ToolTipBase, QColor("#3c3f41")); + new_palette.setColor(QPalette::ToolTipText, QColor("#bbb")); + new_palette.setColor(QPalette::Text, QColor("#bbbbbb")); + new_palette.setColor(QPalette::Button, QColor("#3c3f41")); + new_palette.setColor(QPalette::ButtonText, QColor("#bbbbbb")); + new_palette.setColor(QPalette::Highlight, QColor("#2f65ca")); + new_palette.setColor(QPalette::HighlightedText, QColor("#bbbbbb")); + new_palette.setColor(QPalette::BrightText, QColor("#f0f0f0")); + new_palette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor("#777777")); + new_palette.setColor(QPalette::Disabled, QPalette::WindowText, QColor("#777777")); + new_palette.setColor(QPalette::Disabled, QPalette::Text, QColor("#777777"));; + new_palette.setColor(QPalette::Light, QColor("#3c3f41")); + new_palette.setColor(QPalette::Dark, QColor("#353535")); + } else { + new_palette = style->standardPalette(); + } + qApp->setPalette(new_palette); + style->polish(qApp); + for (auto w : QApplication::allWidgets()) { + w->setPalette(new_palette); + } + } +} +} // namespace utils QString toHex(uint8_t byte) { static std::array hex = []() { @@ -186,11 +216,11 @@ QString toHex(uint8_t byte) { } int num_decimals(double num) { - const QString string = QString::number(num); - const QStringList split = string.split('.'); - if (split.size() == 1) { - return 0; - } else { - return split[1].size(); - } - } + const QString string = QString::number(num); + const QStringList split = string.split('.'); + if (split.size() == 1) { + return 0; + } else { + return split[1].size(); + } +} diff --git a/tools/cabana/util.h b/tools/cabana/util.h index 3cf14982a9..5a3d129a76 100644 --- a/tools/cabana/util.h +++ b/tools/cabana/util.h @@ -1,9 +1,11 @@ #pragma once -#include #include +#include +#include #include +#include #include #include #include @@ -12,23 +14,8 @@ #include #include -#include "tools/cabana/dbc.h" - -class ChangeTracker { -public: - void compute(const QByteArray &dat, double ts, uint32_t freq); - void clear(); - - QVector last_change_t; - QVector colors; - QVector> bit_change_counts; - -private: - const int periodic_threshold = 10; - const int start_alpha = 128; - const float fade_time = 2.0; - QByteArray prev_dat; -}; +#include "tools/cabana/dbc/dbc.h" +#include "tools/cabana/settings.h" class LogSlider : public QSlider { Q_OBJECT @@ -36,17 +23,26 @@ class LogSlider : public QSlider { public: LogSlider(double factor, Qt::Orientation orientation, QWidget *parent = nullptr) : factor(factor), QSlider(orientation, parent) {}; - void setRange(double min, double max) { QSlider::setRange(logScale(min), logScale(max)); } - int value() const { return invLogScale(QSlider::value()); } - void setValue(int value) { QSlider::setValue(logScale(value)); } + void setRange(double min, double max) { + log_min = factor * std::log10(min); + log_max = factor * std::log10(max); + QSlider::setRange(min, max); + setValue(QSlider::value()); + } + int value() const { + double v = log_min + (log_max - log_min) * ((QSlider::value() - minimum()) / double(maximum() - minimum())); + return std::lround(std::pow(10, v / factor)); + } + void setValue(int v) { + double log_v = std::clamp(factor * std::log10(v), log_min, log_max); + v = minimum() + (maximum() - minimum()) * ((log_v - log_min) / (log_max - log_min)); + QSlider::setValue(v); + } private: - double factor; - int logScale(int value) const { return factor * std::log10(value); } - int invLogScale(int value) const { return std::pow(10, value / factor); } + double factor, log_min = 0, log_max = 1; }; - enum { ColorsRole = Qt::UserRole + 1, BytesRole = Qt::UserRole + 2 @@ -68,10 +64,19 @@ private: class MessageBytesDelegate : public QStyledItemDelegate { Q_OBJECT public: - MessageBytesDelegate(QObject *parent); + MessageBytesDelegate(QObject *parent, bool multiple_lines = false); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) override; + void setMultipleLines(bool v); + int widthForBytes(int n) const; + bool multipleLines() const { return multiple_lines; } + +private: QFont fixed_font; - int byte_width; + QSize byte_size = {}; + bool multiple_lines = false; + mutable QSize size_cache[65] = {}; }; inline QString toHex(const QByteArray &dat) { return dat.toHex(' ').toUpper(); } @@ -88,7 +93,33 @@ public: namespace utils { QPixmap icon(const QString &id); +void setTheme(int theme); +inline QString formatSeconds(int seconds) { + return QDateTime::fromTime_t(seconds).toString(seconds > 60 * 60 ? "hh:mm:ss" : "mm:ss"); +} } -QToolButton *toolButton(const QString &icon, const QString &tooltip); +class ToolButton : public QToolButton { + Q_OBJECT +public: + ToolButton(const QString &icon, const QString &tooltip = {}, QWidget *parent = nullptr) : QToolButton(parent) { + setIcon(icon); + setToolTip(tooltip); + setAutoRaise(true); + const int metric = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize); + setIconSize({metric, metric}); + theme = settings.theme; + connect(&settings, &Settings::changed, this, &ToolButton::updateIcon); + } + void setIcon(const QString &icon) { + icon_str = icon; + QToolButton::setIcon(utils::icon(icon_str)); + } + +private: + void updateIcon() { if (std::exchange(theme, settings.theme) != theme) setIcon(icon_str); } + QString icon_str; + int theme; +}; + int num_decimals(double num); diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index 536ae7ba97..53a88972c8 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -1,18 +1,16 @@ #include "tools/cabana/videowidget.h" -#include #include -#include #include #include -#include +#include #include -#include -#include -#include #include #include +const int MIN_VIDEO_HEIGHT = 100; +const int THUMBNAIL_MARGIN = 3; + static const QColor timeline_colors[] = { [(int)TimelineType::None] = QColor(111, 143, 175), [(int)TimelineType::Engaged] = QColor(0, 163, 108), @@ -22,20 +20,23 @@ static const QColor timeline_colors[] = { [(int)TimelineType::AlertCritical] = QColor(199, 0, 57), }; -inline QString formatTime(int seconds) { - return QDateTime::fromTime_t(seconds).toString(seconds > 60 * 60 ? "hh:mm:ss" : "mm:ss"); +bool sortTimelineBasedOnEventPriority(const std::tuple &left, const std::tuple &right){ + const static std::map timelinePriority = { + { TimelineType::None, 0 }, + { TimelineType::Engaged, 10 }, + { TimelineType::AlertInfo, 20 }, + { TimelineType::AlertWarning, 30 }, + { TimelineType::AlertCritical, 40 }, + { TimelineType::UserFlag, 35 } + }; + return timelinePriority.at(std::get<2>(left)) < timelinePriority.at(std::get<2>(right)); } -VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - QFrame *frame = new QFrame(this); - frame->setFrameStyle(QFrame::StyledPanel | QFrame::Plain); - main_layout->addWidget(frame); - - QVBoxLayout *frame_layout = new QVBoxLayout(frame); +VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { + setFrameStyle(QFrame::StyledPanel | QFrame::Plain); + auto main_layout = new QVBoxLayout(this); if (!can->liveStreaming()) { - frame_layout->addWidget(createCameraWidget()); + main_layout->addWidget(createCameraWidget()); } // btn controls @@ -47,8 +48,6 @@ VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) { QButtonGroup *group = new QButtonGroup(this); group->setExclusive(true); for (float speed : {0.1, 0.5, 1., 2.}) { - if (can->liveStreaming() && speed > 1) continue; - QPushButton *btn = new QPushButton(QString("%1x").arg(speed), this); btn->setCheckable(true); QObject::connect(btn, &QPushButton::clicked, [=]() { can->setSpeed(speed); }); @@ -56,11 +55,13 @@ VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) { group->addButton(btn); if (speed == 1.0) btn->setChecked(true); } - frame_layout->addLayout(control_layout); + main_layout->addLayout(control_layout); + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); QObject::connect(play_btn, &QPushButton::clicked, []() { can->pause(!can->isPaused()); }); QObject::connect(can, &AbstractStream::paused, this, &VideoWidget::updatePlayBtnState); QObject::connect(can, &AbstractStream::resume, this, &VideoWidget::updatePlayBtnState); + QObject::connect(&settings, &Settings::changed, this, &VideoWidget::updatePlayBtnState); updatePlayBtnState(); setWhatsThis(tr(R"( @@ -89,13 +90,18 @@ QWidget *VideoWidget::createCameraWidget() { QWidget *w = new QWidget(this); QVBoxLayout *l = new QVBoxLayout(w); l->setContentsMargins(0, 0, 0, 0); - cam_widget = new CameraWidget("camerad", can->visionStreamType(), false); - l->addWidget(cam_widget); + + QStackedLayout *stacked = new QStackedLayout(); + stacked->setStackingMode(QStackedLayout::StackAll); + stacked->addWidget(cam_widget = new CameraWidget("camerad", can->visionStreamType(), false)); + cam_widget->setMinimumHeight(MIN_VIDEO_HEIGHT); + cam_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); + stacked->addWidget(alert_label = new InfoLabel(this)); + l->addLayout(stacked); // slider controls slider_layout = new QHBoxLayout(); - time_label = new ElidedLabel("00:00"); - time_label->setToolTip(tr("Click to set current time")); + time_label = new QLabel("00:00"); slider_layout->addWidget(time_label); slider = new Slider(this); @@ -105,43 +111,17 @@ QWidget *VideoWidget::createCameraWidget() { end_time_label = new QLabel(this); slider_layout->addWidget(end_time_label); l->addLayout(slider_layout); - - cam_widget->setMinimumHeight(100); - cam_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); - - QObject::connect(time_label, &ElidedLabel::clicked, this, &VideoWidget::timeLabelClicked); QObject::connect(slider, &QSlider::sliderReleased, [this]() { can->seekTo(slider->value() / 1000.0); }); - QObject::connect(slider, &QSlider::valueChanged, [=](int value) { time_label->setText(formatTime(value / 1000)); }); + QObject::connect(slider, &QSlider::valueChanged, [=](int value) { time_label->setText(utils::formatSeconds(value / 1000)); }); QObject::connect(cam_widget, &CameraWidget::clicked, []() { can->pause(!can->isPaused()); }); QObject::connect(can, &AbstractStream::updated, this, &VideoWidget::updateState); - QObject::connect(can, &AbstractStream::streamStarted, [this]() { - end_time_label->setText(formatTime(can->totalSeconds())); + QObject::connect(can, &AbstractStream::eventsMerged, [this]() { + end_time_label->setText(utils::formatSeconds(can->totalSeconds())); slider->setRange(0, can->totalSeconds() * 1000); }); return w; } -void VideoWidget::timeLabelClicked() { - auto time_edit = new QTimeEdit(this); - auto init_date_time = can->currentDateTime(); - time_edit->setDateTime(init_date_time); - time_edit->setDisplayFormat("hh:mm:ss"); - time_label->setVisible(false); - slider_layout->insertWidget(0, time_edit); - QTimer::singleShot(0, [=]() { time_edit->setFocus(); }); - - QObject::connect(time_edit, &QTimeEdit::editingFinished, [=]() { - if (time_edit->dateTime() != init_date_time) { - int seconds = can->route()->datetime().secsTo(time_edit->dateTime()); - can->seekTo(seconds); - } - time_edit->setVisible(false); - time_label->setVisible(true); - time_edit->deleteLater(); - }); -} - void VideoWidget::rangeChanged(double min, double max, bool is_zoomed) { if (can->liveStreaming()) return; @@ -149,13 +129,22 @@ void VideoWidget::rangeChanged(double min, double max, bool is_zoomed) { min = 0; max = can->totalSeconds(); } - end_time_label->setText(formatTime(max)); + end_time_label->setText(utils::formatSeconds(max)); slider->setRange(min * 1000, max * 1000); } void VideoWidget::updateState() { - if (!slider->isSliderDown()) + if (!slider->isSliderDown()) { slider->setValue(can->currentSec() * 1000); + } + std::lock_guard lk(slider->thumbnail_lock); + uint64_t mono_time = (can->currentSec() + can->routeStartTime()) * 1e9; + auto it = slider->alerts.lower_bound(mono_time); + if (it != slider->alerts.end() && (it->first - mono_time) < 1e9) { + alert_label->showAlert(it->second); + } else { + alert_label->showAlert({}); + } } void VideoWidget::updatePlayBtnState() { @@ -164,16 +153,13 @@ void VideoWidget::updatePlayBtnState() { } // Slider -Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) { - QTimer *timer = new QTimer(this); - timer->setInterval(2000); - timer->callOnTimeout([this]() { +Slider::Slider(QWidget *parent) : timer(this), thumbnail_label(parent), QSlider(Qt::Horizontal, parent) { + timer.callOnTimeout([this]() { timeline = can->getTimeline(); + std::sort(timeline.begin(), timeline.end(), sortTimelineBasedOnEventPriority); update(); }); setMouseTracking(true); - - QObject::connect(can, SIGNAL(streamStarted()), timer, SLOT(start())); QObject::connect(can, &AbstractStream::streamStarted, this, &Slider::streamStarted); } @@ -187,41 +173,38 @@ void Slider::streamStarted() { thumnail_future.waitForFinished(); abort_load_thumbnail = false; thumbnails.clear(); + timeline.clear(); + timer.start(2000); thumnail_future = QtConcurrent::run(this, &Slider::loadThumbnails); } void Slider::loadThumbnails() { - const auto segments = can->route()->segments(); + const auto &segments = can->route()->segments(); for (auto it = segments.rbegin(); it != segments.rend() && !abort_load_thumbnail; ++it) { + LogReader log; std::string qlog = it->second.qlog.toStdString(); - if (!qlog.empty()) { - LogReader log; - if (log.load(qlog, &abort_load_thumbnail, {cereal::Event::Which::THUMBNAIL}, true, 0, 3)) { - for (auto ev = log.events.cbegin(); ev != log.events.cend() && !abort_load_thumbnail; ++ev) { + if (!qlog.empty() && log.load(qlog, &abort_load_thumbnail, {cereal::Event::Which::THUMBNAIL, cereal::Event::Which::CONTROLS_STATE}, true, 0, 3)) { + for (auto ev = log.events.cbegin(); ev != log.events.cend() && !abort_load_thumbnail; ++ev) { + if ((*ev)->which == cereal::Event::Which::THUMBNAIL) { auto thumb = (*ev)->event.getThumbnail(); - QString str = getThumbnailString(thumb.getThumbnail()); - std::lock_guard lk(thumbnail_lock); - thumbnails[thumb.getTimestampEof()] = str; + auto data = thumb.getThumbnail(); + if (QPixmap pm; pm.loadFromData(data.begin(), data.size(), "jpeg")) { + pm = pm.scaledToHeight(MIN_VIDEO_HEIGHT - THUMBNAIL_MARGIN * 2, Qt::SmoothTransformation); + std::lock_guard lk(thumbnail_lock); + thumbnails[thumb.getTimestampEof()] = pm; + } + } else if ((*ev)->which == cereal::Event::Which::CONTROLS_STATE) { + auto cs = (*ev)->event.getControlsState(); + if (cs.getAlertType().size() > 0 && cs.getAlertText1().size() > 0) { + std::lock_guard lk(thumbnail_lock); + alerts.emplace((*ev)->mono_time, AlertInfo{cs.getAlertStatus(), cs.getAlertText1().cStr(), cs.getAlertText2().cStr()}); + } } } } } } -QString Slider::getThumbnailString(const capnp::Data::Reader &data) { - QPixmap thumb; - if (thumb.loadFromData(data.begin(), data.size(), "jpeg")) { - thumb = thumb.scaled({thumb.width()/3, thumb.height()/3}, Qt::KeepAspectRatio); - thumbnail_size = thumb.size(); - QByteArray bytes; - QBuffer buffer(&bytes); - buffer.open(QIODevice::WriteOnly); - thumb.save(&buffer, "png"); - return QString("").arg(QString(bytes.toBase64())); - } - return {}; -} - void Slider::sliderChange(QAbstractSlider::SliderChange change) { if (change == QAbstractSlider::SliderValueChange) { int x = width() * ((value() - minimum()) / double(maximum() - minimum())); @@ -240,6 +223,7 @@ void Slider::paintEvent(QPaintEvent *ev) { p.fillRect(r, timeline_colors[(int)TimelineType::None]); double min = minimum() / 1000.0; double max = maximum() / 1000.0; + for (auto [begin, end, type] : timeline) { if (begin > max || end < min) continue; @@ -267,16 +251,97 @@ void Slider::mousePressEvent(QMouseEvent *e) { } void Slider::mouseMoveEvent(QMouseEvent *e) { - QString thumb; + QPixmap thumb; + AlertInfo alert; + double seconds = (minimum() + e->pos().x() * ((maximum() - minimum()) / (double)width())) / 1000.0; { - double seconds = (minimum() + e->pos().x() * ((maximum() - minimum()) / (double)width())) / 1000.0; std::lock_guard lk(thumbnail_lock); - auto it = thumbnails.lowerBound((seconds + can->routeStartTime()) * 1e9); - if (it != thumbnails.end()) { - thumb = it.value(); + uint64_t mono_time = (seconds + can->routeStartTime()) * 1e9; + auto it = thumbnails.lowerBound(mono_time); + if (it != thumbnails.end()) thumb = it.value(); + auto alert_it = alerts.lower_bound(mono_time); + if (alert_it != alerts.end() && (alert_it->first - mono_time) < 1e9) { + alert = alert_it->second; } } - QPoint pt = mapToGlobal({e->pos().x() - thumbnail_size.width() / 2, -thumbnail_size.height() - 30}); - QToolTip::showText(pt, thumb, this, rect()); + int x = std::clamp(e->pos().x() - thumb.width() / 2, THUMBNAIL_MARGIN, rect().right() - thumb.width() - THUMBNAIL_MARGIN); + int y = -thumb.height(); + thumbnail_label.showPixmap(mapToParent({x, y}), utils::formatSeconds(seconds), thumb, alert); QSlider::mouseMoveEvent(e); } + +bool Slider::event(QEvent *event) { + switch (event->type()) { + case QEvent::WindowActivate: + case QEvent::WindowDeactivate: + case QEvent::FocusIn: + case QEvent::FocusOut: + case QEvent::Leave: + thumbnail_label.hide(); + break; + default: + break; + } + return QSlider::event(event); +} + +// InfoLabel + +InfoLabel::InfoLabel(QWidget *parent) : QWidget(parent, Qt::WindowStaysOnTopHint) { + setAttribute(Qt::WA_ShowWithoutActivating); + setVisible(false); +} + +void InfoLabel::showPixmap(const QPoint &pt, const QString &sec, const QPixmap &pm, const AlertInfo &alert) { + pixmap = pm; + second = sec; + alert_info = alert; + setVisible(!pm.isNull()); + if (isVisible()) { + resize(pm.size()); + move(pt); + update(); + } +} + +void InfoLabel::showAlert(const AlertInfo &alert) { + alert_info = alert; + pixmap = {}; + setVisible(!alert_info.text1.isEmpty()); + if (isVisible()) { + update(); + } +} + +void InfoLabel::paintEvent(QPaintEvent *event) { + QPainter p(this); + p.setPen(QPen(palette().color(QPalette::BrightText), 2)); + if (!pixmap.isNull()) { + p.drawPixmap(0, 0, pixmap); + p.drawRect(rect()); + p.drawText(rect().adjusted(0, 0, 0, -THUMBNAIL_MARGIN), second, Qt::AlignHCenter | Qt::AlignBottom); + } + if (alert_info.text1.size() > 0) { + QColor color = timeline_colors[(int)TimelineType::AlertInfo]; + if (alert_info.status == cereal::ControlsState::AlertStatus::USER_PROMPT) { + color = timeline_colors[(int)TimelineType::AlertWarning]; + } else if (alert_info.status == cereal::ControlsState::AlertStatus::CRITICAL) { + color = timeline_colors[(int)TimelineType::AlertCritical]; + } + color.setAlphaF(0.5); + QString text = alert_info.text1; + if (!alert_info.text2.isEmpty()) { + text += "\n" + alert_info.text2; + } + + if (!pixmap.isNull()) { + QFont font; + font.setPixelSize(11); + p.setFont(font); + } + QRect text_rect = rect().adjusted(2, 2, -2, -2); + QRect r = p.fontMetrics().boundingRect(text_rect, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, text); + p.fillRect(text_rect.left(), r.top(), text_rect.width(), r.height(), color); + p.drawText(text_rect, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, text); + } +} diff --git a/tools/cabana/videowidget.h b/tools/cabana/videowidget.h index d10b09fec5..960053f9a3 100644 --- a/tools/cabana/videowidget.h +++ b/tools/cabana/videowidget.h @@ -8,12 +8,28 @@ #include #include #include +#include #include "selfdrive/ui/qt/widgets/cameraview.h" -#include "selfdrive/ui/qt/widgets/controls.h" -#include "tools/cabana/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" +struct AlertInfo { + cereal::ControlsState::AlertStatus status; + QString text1; + QString text2; +}; + +class InfoLabel : public QWidget { +public: + InfoLabel(QWidget *parent); + void showPixmap(const QPoint &pt, const QString &sec, const QPixmap &pm, const AlertInfo &alert); + void showAlert(const AlertInfo &alert); + void paintEvent(QPaintEvent *event) override; + QPixmap pixmap; + QString second; + AlertInfo alert_info; +}; + class Slider : public QSlider { Q_OBJECT @@ -24,22 +40,25 @@ public: private: void mousePressEvent(QMouseEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; + bool event(QEvent *event) override; void sliderChange(QAbstractSlider::SliderChange change) override; void paintEvent(QPaintEvent *ev) override; void streamStarted(); void loadThumbnails(); - QString getThumbnailString(const capnp::Data::Reader &data); int slider_x = -1; std::vector> timeline; std::mutex thumbnail_lock; std::atomic abort_load_thumbnail = false; - QMap thumbnails; + QMap thumbnails; + std::map alerts; QFuture thumnail_future; - QSize thumbnail_size = {}; + InfoLabel thumbnail_label; + QTimer timer; + friend class VideoWidget; }; -class VideoWidget : public QWidget { +class VideoWidget : public QFrame { Q_OBJECT public: @@ -49,13 +68,13 @@ public: protected: void updateState(); void updatePlayBtnState(); - void timeLabelClicked(); QWidget *createCameraWidget(); CameraWidget *cam_widget; QLabel *end_time_label; - ElidedLabel *time_label; + QLabel *time_label; QHBoxLayout *slider_layout; QPushButton *play_btn; + InfoLabel *alert_label; Slider *slider; }; diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index dd2041e0cb..67ca098c00 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -45,7 +45,7 @@ brew "libarchive" brew "libusb" brew "libtool" brew "llvm" -brew "openssl" +brew "openssl@3.0" brew "pyenv" brew "qt@5" brew "zeromq" diff --git a/tools/plotjuggler/layouts/camera-timings.xml b/tools/plotjuggler/layouts/camera-timings.xml new file mode 100644 index 0000000000..5cc1650909 --- /dev/null +++ b/tools/plotjuggler/layouts/camera-timings.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/plotjuggler/layouts/ublox-debug.xml b/tools/plotjuggler/layouts/ublox-debug.xml new file mode 100644 index 0000000000..d595a9ecc7 --- /dev/null +++ b/tools/plotjuggler/layouts/ublox-debug.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/replay/README.md b/tools/replay/README.md index 2d0b702bd0..6c2cdf9521 100644 --- a/tools/replay/README.md +++ b/tools/replay/README.md @@ -5,16 +5,16 @@ `replay` replays all the messages logged while running openpilot. ```bash -# Log in via browser to have access to non-public routes +# Log in via browser to have access to routes from your comma account python tools/lib/auth.py # Start a replay tools/replay/replay # Example: -# tools/replay/replay '4cf7a6ad03080c90|2021-09-29--13-46-36' +tools/replay/replay '4cf7a6ad03080c90|2021-09-29--13-46-36' # or use --demo to replay the default demo route: -# tools/replay/replay --demo +tools/replay/replay --demo # watch the replay with the normal openpilot UI cd selfdrive/ui && ./ui @@ -64,12 +64,24 @@ cd selfdrive/ui && ./watch3 Replay CAN messages as they were recorded using a [panda jungle](https://comma.ai/shop/products/panda-jungle). The jungle has 6x OBD-C ports for connecting all your comma devices. Check out the [jungle repo](https://github.com/commaai/panda_jungle) for more info. -`can_replay.py` is a convenient script for when any CAN data will do. +In order to run your device as if it was in a car: +* connect a panda jungle to your PC +* connect a comma device or panda to the jungle via OBD-C +* run `can_replay.py` -In order to replay specific route: -```bash -MOCK=1 selfdrive/boardd/tests/boardd_old.py +``` bash +batman:replay$ ./can_replay.py -h +usage: can_replay.py [-h] [route_or_segment_name] -# In another terminal: -tools/replay/replay +Replay CAN messages from a route to all connected pandas and jungles +in a loop. + +positional arguments: + route_or_segment_name + The route or segment name to replay. If not + specified, a default public route will be + used. (default: None) + +optional arguments: + -h, --help show this help message and exit ``` diff --git a/tools/replay/can_replay.py b/tools/replay/can_replay.py index 0b8b4fe0a1..52666c4db0 100755 --- a/tools/replay/can_replay.py +++ b/tools/replay/can_replay.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import argparse import os import time import threading @@ -11,6 +12,7 @@ from common.basedir import BASEDIR from common.realtime import config_realtime_process, Ratekeeper, DT_CTRL from selfdrive.boardd.boardd import can_capnp_to_can_list from tools.plotjuggler.juggle import load_segment +from tools.lib.logreader import logreader_from_route_or_segment from panda import Panda try: @@ -87,18 +89,27 @@ def connect(): if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Replay CAN messages from a route to all connected pandas and jungles in a loop.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument("route_or_segment_name", nargs='?', help="The route or segment name to replay. If not specified, a default public route will be used.") + args = parser.parse_args() + if not panda_jungle_imported: print("\33[31m", "WARNING: cannot connect to jungles. Clone the jungle library to enable support:", "\033[0m") print("\033[34m", f"cd {BASEDIR} && git clone https://github.com/commaai/panda_jungle", "\033[0m") print("Loading log...") - ROUTE = "77611a1fac303767/2020-03-24--09-50-38" - REPLAY_SEGS = list(range(10, 16)) # route has 82 segments available - CAN_MSGS = [] - logs = [f"https://commadataci.blob.core.windows.net/openpilotci/{ROUTE}/{i}/rlog.bz2" for i in REPLAY_SEGS] - with multiprocessing.Pool(24) as pool: - for lr in tqdm(pool.map(load_segment, logs)): - CAN_MSGS += [can_capnp_to_can_list(m.can) for m in lr if m.which() == 'can'] + if args.route_or_segment_name is None: + ROUTE = "77611a1fac303767/2020-03-24--09-50-38" + REPLAY_SEGS = list(range(10, 16)) # route has 82 segments available + CAN_MSGS = [] + logs = [f"https://commadataci.blob.core.windows.net/openpilotci/{ROUTE}/{i}/rlog.bz2" for i in REPLAY_SEGS] + with multiprocessing.Pool(24) as pool: + for lr in tqdm(pool.map(load_segment, logs)): + CAN_MSGS += [can_capnp_to_can_list(m.can) for m in lr if m.which() == 'can'] + else: + lr = logreader_from_route_or_segment(args.route_or_segment_name) + CAN_MSGS = [can_capnp_to_can_list(m.can) for m in lr if m.which() == 'can'] # set both to cycle ignition IGN_ON = int(os.getenv("ON", "0")) diff --git a/tools/replay/framereader.cc b/tools/replay/framereader.cc index a1ff7b9f8e..ed276c627c 100644 --- a/tools/replay/framereader.cc +++ b/tools/replay/framereader.cc @@ -68,14 +68,20 @@ FrameReader::~FrameReader() { bool FrameReader::load(const std::string &url, bool no_hw_decoder, std::atomic *abort, bool local_cache, int chunk_size, int retries) { FileReader f(local_cache, chunk_size, retries); std::string data = f.read(url, abort); - if (data.empty()) return false; + if (data.empty()) { + rWarning("URL %s returned no data", url.c_str()); + return false; + } return load((std::byte *)data.data(), data.size(), no_hw_decoder, abort); } bool FrameReader::load(const std::byte *data, size_t size, bool no_hw_decoder, std::atomic *abort) { input_ctx = avformat_alloc_context(); - if (!input_ctx) return false; + if (!input_ctx) { + rError("Error calling avformat_alloc_context"); + return false; + } struct buffer_data bd = { .data = (const uint8_t*)data, @@ -121,7 +127,10 @@ bool FrameReader::load(const std::byte *data, size_t size, bool no_hw_decoder, s } ret = avcodec_open2(decoder_ctx, decoder, nullptr); - if (ret < 0) return false; + if (ret < 0) { + rError("avcodec_open2 failed %d", ret); + return false; + } packets.reserve(60 * 20); // 20fps, one minute while (!(abort && *abort)) { diff --git a/tools/sim/Dockerfile.sim b/tools/sim/Dockerfile.sim index be16f8c863..48aa12ebc6 100644 --- a/tools/sim/Dockerfile.sim +++ b/tools/sim/Dockerfile.sim @@ -14,6 +14,7 @@ RUN mkdir -p $HOME/openpilot COPY SConstruct $HOME/openpilot/ +COPY ./body $HOME/openpilot/body COPY ./third_party $HOME/openpilot/third_party COPY ./site_scons $HOME/openpilot/site_scons COPY ./rednose $HOME/openpilot/rednose diff --git a/tools/ubuntu_setup.sh b/tools/ubuntu_setup.sh index 71bad2e8a2..97b706de5b 100755 --- a/tools/ubuntu_setup.sh +++ b/tools/ubuntu_setup.sh @@ -53,6 +53,8 @@ function install_ubuntu_common_requirements() { libgles2-mesa-dev \ libglfw3-dev \ libglib2.0-0 \ + libncurses5-dev \ + libncursesw5-dev \ libomp-dev \ libopencv-dev \ libpng16-16 \ diff --git a/update_requirements.sh b/update_requirements.sh index b430df59e5..b2b36e7097 100755 --- a/update_requirements.sh +++ b/update_requirements.sh @@ -50,25 +50,27 @@ pip install poetry==1.2.2 poetry config virtualenvs.prefer-active-python true --local -POETRY_INSTALL_ARGS="" -if [ -d "./xx" ] || [ -n "$XX" ]; then - echo "WARNING: using xx dependency group, installing globally" - poetry config virtualenvs.create false --local - POETRY_INSTALL_ARGS="--with xx --sync" +if [[ -n "$XX" ]] || [[ "$(basename "$(dirname "$(pwd)")")" == "xx" ]]; then + XX=true fi -echo "pip packages install..." -poetry install --no-cache --no-root $POETRY_INSTALL_ARGS -pyenv rehash +POETRY_INSTALL_ARGS="--no-cache --no-root" -if [ -d "./xx" ] || [ -n "$POETRY_VIRTUALENVS_CREATE" ]; then - RUN="" +if [ -n "$XX" ]; then + echo "WARNING: using xx dependency group, installing globally" + poetry config virtualenvs.create false --local + POETRY_INSTALL_ARGS="$POETRY_INSTALL_ARGS --with xx --sync" else echo "PYTHONPATH=${PWD}" > .env poetry self add poetry-dotenv-plugin@^0.1.0 - RUN="poetry run" fi +echo "pip packages install..." +poetry install $POETRY_INSTALL_ARGS +pyenv rehash + +[ -n "$XX" ] || [ -n "$POETRY_VIRTUALENVS_CREATE" ] && RUN="" || RUN="poetry run" + if [ "$(uname)" != "Darwin" ]; then echo "pre-commit hooks install..." shopt -s nullglob